Project 10: Azure App Service & Web Apps β
Overview β
Deploy and manage web applications using Azure App Service with deployment slots, custom domains, SSL certificates, scaling, and CI/CD integration. This covers key AZ-104 compute objectives.
Difficulty: Intermediate
Duration: 3-4 hours
Cost: ~$50-100/month (Standard tier for slots)
Exam Weight: Part of Compute domain (20-25%)
Architecture Diagram β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β CI/CD PIPELINE β
β β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ β
β β GitHub βββββΆβ GitHub βββββΆβ Build & βββββΆβ Deploy to β β
β β Repository β β Actions β β Test β β Staging β β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ β
β β β
β ββββββββββββ β
β β β
β βΌ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β APP SERVICE ENVIRONMENT β
β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β APP SERVICE PLAN (asp-webapp-prod) β β
β β SKU: Standard S1 (Supports Slots) β β
β β Workers: 3 instances (manual/auto-scale) β β
β β β β
β β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β
β β β WEB APP (webapp-prod-12345) β β β
β β β β β β
β β β ββββββββββββββββββββββββββββββ ββββββββββββββββββββββββββββββββββ β β β
β β β β PRODUCTION SLOT β β STAGING SLOT β β β β
β β β β (webapp-prod-12345) β β (webapp-prod-12345-staging) β β β β
β β β β β β β β β β
β β β β URL: β β URL: β β β β
β β β β webapp-prod-12345. β β webapp-prod-12345-staging. β β β β
β β β β azurewebsites.net β β azurewebsites.net β β β β
β β β β β β β β β β
β β β β Custom Domain: β β Purpose: β β β β
β β β β www.contoso.com β β - Testing β β β β
β β β β β β - Warm-up β β β β
β β β β SSL: Managed Certificate β β - Blue-green deploy β β β β
β β β β β β β β β β
β β β β Traffic: 100% β β Traffic: 0% β β β β
β β β β β β (until swap) β β β β
β β β ββββββββββββββββββββββββββββββ ββββββββββββββββββββββββββββββββββ β β β
β β β β β β β β
β β β βββββββββββ¬ββββββββββββββββ β β β
β β β β β β β
β β β SLOT SWAP β β β
β β β (Zero Downtime) β β β
β β β β β β
β β β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β β
β β β β APPLICATION SETTINGS β β β β
β β β β β β β β
β β β β Slot Settings (sticky): Swapped Settings: β β β β
β β β β - SLOT_NAME=production - DB_CONNECTION_STRING β β β β
β β β β - APPINSIGHTS_KEY - API_KEY β β β β
β β β β - DEBUG_MODE=false - FEATURE_FLAGS β β β β
β β β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β β
β β β β β β
β β β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β β
β β β β SCALING CONFIGURATION β β β β
β β β β β β β β
β β β β Auto-scale Rules: β β β β
β β β β - Scale out: CPU > 70% β Add 1 instance (max 10) β β β β
β β β β - Scale in: CPU < 30% β Remove 1 instance (min 2) β β β β
β β β β - Schedule: Weekdays 9-5 β Min 3 instances β β β β
β β β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β β
β β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β
β β β β
β β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β
β β β WEB APP (webapp-api-12345) β β β
β β β API Backend Service β β β
β β β β β β
β β β Runtime: .NET 8 / Node.js 20 / Python 3.11 β β β
β β β Always On: Enabled β β β
β β β HTTPS Only: Enabled β β β
β β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β NETWORKING & SECURITY β
β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β VNET INTEGRATION β β
β β β β
β β App Service βββββββΆ VNet (10.0.0.0/16) βββββββΆ Private Resources β β
β β (Outbound) βββ Integration Subnet - Azure SQL β β
β β (10.0.1.0/24) - Storage Account β β
β β - Redis Cache β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β PRIVATE ENDPOINT (Optional) β β
β β β β
β β VNet βββββββΆ Private Endpoint βββββββΆ App Service (Inbound) β β
β β (10.0.2.4) webapp-prod-12345. β β
β β privatelink.azurewebsites.net β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Deployment Flow:
ββββββββββββ ββββββββββββ ββββββββββββ ββββββββββββ ββββββββββββ
β Code βββββΆβ Build βββββΆβ Deploy βββββΆβ Test on βββββΆβ Swap to β
β Push β β CI β β Staging β β Staging β β Prod β
ββββββββββββ ββββββββββββ ββββββββββββ ββββββββββββ ββββββββββββDeployment Slot Limits by Tier β
| Tier | Max Slots | Use Case |
|---|---|---|
| Free/Shared | 0 | Development only |
| Basic | 0 | Small workloads |
| Standard | 5 | Production workloads |
| Premium | 20 | Enterprise workloads |
| Isolated | 20 | High security |
What You'll Learn β
- Create App Service plans and web apps
- Configure deployment slots
- Perform slot swaps (zero downtime)
- Configure auto-scaling rules
- Set up custom domains and SSL
- Configure VNet integration
- Implement CI/CD with GitHub Actions
Phase 1: Create App Service Infrastructure β
Step 1.1: Create Resource Group β
bash
# Set variables
LOCATION="eastus"
RG_NAME="rg-appservice-lab"
PLAN_NAME="asp-webapp-prod"
APP_NAME="webapp-$(date +%s | tail -c 8)"
# Create resource group
az group create \
--name $RG_NAME \
--location $LOCATION \
--tags Project=AppService Environment=Lab
echo "Resource group created"Step 1.2: Create App Service Plan β
bash
# Create App Service Plan (Standard S1 for deployment slots)
az appservice plan create \
--name $PLAN_NAME \
--resource-group $RG_NAME \
--sku S1 \
--is-linux \
--location $LOCATION
echo "App Service Plan created: $PLAN_NAME"Step 1.3: Create Web App β
bash
# Create Web App with Node.js runtime
az webapp create \
--resource-group $RG_NAME \
--plan $PLAN_NAME \
--name $APP_NAME \
--runtime "NODE:20-lts"
# Enable HTTPS only
az webapp update \
--resource-group $RG_NAME \
--name $APP_NAME \
--https-only true
# Get web app URL
APP_URL=$(az webapp show \
--resource-group $RG_NAME \
--name $APP_NAME \
--query defaultHostName -o tsv)
echo "Web App created: https://$APP_URL"Phase 2: Deploy Sample Application β
Step 2.1: Create Sample Node.js App β
bash
# Create project directory
mkdir -p webapp-demo
cd webapp-demo
# Create package.json
cat > package.json << 'EOF'
{
"name": "azure-webapp-demo",
"version": "1.0.0",
"description": "AZ-104 App Service Demo",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^4.18.2"
}
}
EOF
# Create server.js
cat > server.js << 'EOF'
const express = require('express');
const app = express();
const port = process.env.PORT || 8080;
// Environment variables
const slotName = process.env.SLOT_NAME || 'production';
const version = process.env.APP_VERSION || '1.0.0';
const environment = process.env.NODE_ENV || 'development';
app.get('/', (req, res) => {
res.json({
message: 'Hello from Azure App Service!',
slot: slotName,
version: version,
environment: environment,
hostname: require('os').hostname(),
timestamp: new Date().toISOString()
});
});
app.get('/health', (req, res) => {
res.status(200).json({ status: 'healthy', slot: slotName });
});
app.listen(port, () => {
console.log(`Server running on port ${port}`);
console.log(`Slot: ${slotName}, Version: ${version}`);
});
EOF
echo "Sample application created"Step 2.2: Deploy Using ZIP Deploy β
bash
# Create ZIP file
zip -r app.zip package.json server.js
# Deploy to App Service
az webapp deployment source config-zip \
--resource-group $RG_NAME \
--name $APP_NAME \
--src app.zip
# Test the deployment
curl https://$APP_URL
echo "Application deployed"Phase 3: Configure Application Settings β
Step 3.1: Set Application Settings β
bash
# Set application settings
az webapp config appsettings set \
--resource-group $RG_NAME \
--name $APP_NAME \
--settings \
SLOT_NAME=production \
APP_VERSION=1.0.0 \
NODE_ENV=production
# List all settings
az webapp config appsettings list \
--resource-group $RG_NAME \
--name $APP_NAME \
--output tableStep 3.2: Configure Connection Strings β
bash
# Set connection string (example)
az webapp config connection-string set \
--resource-group $RG_NAME \
--name $APP_NAME \
--connection-string-type SQLAzure \
--settings \
"DefaultConnection=Server=tcp:myserver.database.windows.net;Database=mydb;User ID=admin;Password=xxx"
echo "Connection strings configured"Step 3.3: Configure General Settings β
bash
# Enable Always On (prevents cold starts)
az webapp config set \
--resource-group $RG_NAME \
--name $APP_NAME \
--always-on true \
--min-tls-version 1.2 \
--ftps-state Disabled \
--http20-enabled true
echo "General settings configured"Phase 4: Create Deployment Slots β
Step 4.1: Create Staging Slot β
bash
# Create staging deployment slot
az webapp deployment slot create \
--resource-group $RG_NAME \
--name $APP_NAME \
--slot staging
# Get staging URL
STAGING_URL=$(az webapp deployment slot show \
--resource-group $RG_NAME \
--name $APP_NAME \
--slot staging \
--query defaultHostName -o tsv)
echo "Staging slot created: https://$STAGING_URL"Step 4.2: Configure Staging Slot Settings β
bash
# Set staging-specific settings (slot settings = sticky)
az webapp config appsettings set \
--resource-group $RG_NAME \
--name $APP_NAME \
--slot staging \
--slot-settings SLOT_NAME=staging \
--settings \
APP_VERSION=2.0.0 \
NODE_ENV=staging
# Note: SLOT_NAME has --slot-settings flag, making it "sticky" (doesn't swap)
echo "Staging settings configured"Step 4.3: Deploy New Version to Staging β
bash
# Update version in server.js
cat > server.js << 'EOF'
const express = require('express');
const app = express();
const port = process.env.PORT || 8080;
const slotName = process.env.SLOT_NAME || 'production';
const version = process.env.APP_VERSION || '2.0.0';
const environment = process.env.NODE_ENV || 'development';
app.get('/', (req, res) => {
res.json({
message: 'Hello from Azure App Service - Version 2!',
slot: slotName,
version: version,
environment: environment,
hostname: require('os').hostname(),
timestamp: new Date().toISOString(),
newFeature: 'This is a new feature in v2!'
});
});
app.get('/health', (req, res) => {
res.status(200).json({ status: 'healthy', slot: slotName, version: version });
});
app.listen(port, () => {
console.log(`Server running on port ${port}`);
console.log(`Slot: ${slotName}, Version: ${version}`);
});
EOF
# Repackage and deploy to staging
zip -r app.zip package.json server.js
az webapp deployment source config-zip \
--resource-group $RG_NAME \
--name $APP_NAME \
--slot staging \
--src app.zip
# Test staging
echo "Production: $(curl -s https://$APP_URL | jq -r '.version')"
echo "Staging: $(curl -s https://$STAGING_URL | jq -r '.version')"Phase 5: Slot Swap Operations β
Step 5.1: Preview Slot Swap (What-If) β
bash
# Preview what will change in swap
az webapp deployment slot swap \
--resource-group $RG_NAME \
--name $APP_NAME \
--slot staging \
--target-slot production \
--action preview
echo "Review the changes above before swapping"Step 5.2: Perform Slot Swap β
bash
# Swap staging to production
az webapp deployment slot swap \
--resource-group $RG_NAME \
--name $APP_NAME \
--slot staging \
--target-slot production
# Verify swap completed
echo "Production version: $(curl -s https://$APP_URL | jq -r '.version')"
echo "Staging version: $(curl -s https://$STAGING_URL | jq -r '.version')"
# Note: Versions should be swapped now!Step 5.3: Rollback (Swap Back) β
bash
# If issues found, swap back immediately
az webapp deployment slot swap \
--resource-group $RG_NAME \
--name $APP_NAME \
--slot staging \
--target-slot production
echo "Rollback completed"Step 5.4: Configure Auto-Swap (Optional) β
bash
# Enable auto-swap from staging to production
az webapp deployment slot auto-swap \
--resource-group $RG_NAME \
--name $APP_NAME \
--slot staging \
--auto-swap-slot production
echo "Auto-swap enabled for staging slot"Phase 6: Configure Scaling β
Step 6.1: Manual Scaling β
bash
# Scale out to 3 instances
az appservice plan update \
--resource-group $RG_NAME \
--name $PLAN_NAME \
--number-of-workers 3
# Verify scaling
az appservice plan show \
--resource-group $RG_NAME \
--name $PLAN_NAME \
--query "sku.capacity" -o tsvStep 6.2: Configure Auto-Scaling β
bash
# Get App Service Plan resource ID
PLAN_ID=$(az appservice plan show \
--resource-group $RG_NAME \
--name $PLAN_NAME \
--query id -o tsv)
# Create autoscale settings
az monitor autoscale create \
--resource-group $RG_NAME \
--resource $PLAN_ID \
--resource-type Microsoft.Web/serverfarms \
--name "autoscale-webapp" \
--min-count 2 \
--max-count 10 \
--count 2
# Add scale-out rule (CPU > 70%)
az monitor autoscale rule create \
--resource-group $RG_NAME \
--autoscale-name "autoscale-webapp" \
--condition "CpuPercentage > 70 avg 5m" \
--scale out 1
# Add scale-in rule (CPU < 30%)
az monitor autoscale rule create \
--resource-group $RG_NAME \
--autoscale-name "autoscale-webapp" \
--condition "CpuPercentage < 30 avg 5m" \
--scale in 1
echo "Auto-scaling configured"Step 6.3: Configure Schedule-Based Scaling β
bash
# Add schedule rule for business hours
az monitor autoscale profile create \
--resource-group $RG_NAME \
--autoscale-name "autoscale-webapp" \
--name "business-hours" \
--min-count 3 \
--max-count 10 \
--count 5 \
--timezone "Eastern Standard Time" \
--start "09:00" \
--end "17:00" \
--recurrence week Mon Tue Wed Thu Fri
echo "Schedule-based scaling configured"Phase 7: Configure Networking β
Step 7.1: Create VNet for Integration β
bash
# Create VNet
az network vnet create \
--resource-group $RG_NAME \
--name vnet-appservice \
--address-prefix 10.0.0.0/16 \
--subnet-name snet-integration \
--subnet-prefix 10.0.1.0/24 \
--location $LOCATION
# Delegate subnet to App Service
az network vnet subnet update \
--resource-group $RG_NAME \
--vnet-name vnet-appservice \
--name snet-integration \
--delegations Microsoft.Web/serverFarms
echo "VNet created for App Service integration"Step 7.2: Configure VNet Integration β
bash
# Enable VNet integration
az webapp vnet-integration add \
--resource-group $RG_NAME \
--name $APP_NAME \
--vnet vnet-appservice \
--subnet snet-integration
# Verify integration
az webapp vnet-integration list \
--resource-group $RG_NAME \
--name $APP_NAME \
--output table
echo "VNet integration configured"Step 7.3: Configure Access Restrictions β
bash
# Allow only specific IP range
az webapp config access-restriction add \
--resource-group $RG_NAME \
--name $APP_NAME \
--rule-name "AllowOfficeIP" \
--action Allow \
--ip-address "203.0.113.0/24" \
--priority 100
# Allow Azure Front Door
az webapp config access-restriction add \
--resource-group $RG_NAME \
--name $APP_NAME \
--rule-name "AllowFrontDoor" \
--action Allow \
--service-tag AzureFrontDoor.Backend \
--priority 200
# List restrictions
az webapp config access-restriction show \
--resource-group $RG_NAME \
--name $APP_NAME \
--output tablePhase 8: Configure Custom Domain & SSL β
Step 8.1: Add Custom Domain (requires domain ownership) β
bash
# Add custom domain (example - requires DNS verification)
# az webapp config hostname add \
# --resource-group $RG_NAME \
# --webapp-name $APP_NAME \
# --hostname "www.yourdomain.com"
echo "Custom domain steps:"
echo "1. Add CNAME record: www -> $APP_URL"
echo "2. Add TXT record for verification"
echo "3. Run: az webapp config hostname add"Step 8.2: Configure Managed SSL Certificate β
bash
# Create managed certificate (after domain is added)
# az webapp config ssl create \
# --resource-group $RG_NAME \
# --name $APP_NAME \
# --hostname "www.yourdomain.com"
# Bind certificate
# az webapp config ssl bind \
# --resource-group $RG_NAME \
# --name $APP_NAME \
# --certificate-thumbprint <thumbprint> \
# --ssl-type SNIPhase 9: Configure Logging & Monitoring β
Step 9.1: Enable Application Logging β
bash
# Enable application logging
az webapp log config \
--resource-group $RG_NAME \
--name $APP_NAME \
--application-logging filesystem \
--level information \
--detailed-error-messages true \
--failed-request-tracing true \
--web-server-logging filesystem
echo "Application logging enabled"Step 9.2: Stream Logs β
bash
# Stream logs in real-time
az webapp log tail \
--resource-group $RG_NAME \
--name $APP_NAME
# Download logs
az webapp log download \
--resource-group $RG_NAME \
--name $APP_NAME \
--log-file webapp-logs.zipStep 9.3: Configure Diagnostic Settings β
bash
# Create Log Analytics workspace
WORKSPACE_NAME="law-appservice"
az monitor log-analytics workspace create \
--resource-group $RG_NAME \
--workspace-name $WORKSPACE_NAME \
--location $LOCATION
# Get workspace ID
WORKSPACE_ID=$(az monitor log-analytics workspace show \
--resource-group $RG_NAME \
--workspace-name $WORKSPACE_NAME \
--query id -o tsv)
# Enable diagnostic settings
APP_ID=$(az webapp show -g $RG_NAME -n $APP_NAME --query id -o tsv)
az monitor diagnostic-settings create \
--resource $APP_ID \
--name "webapp-diagnostics" \
--workspace $WORKSPACE_ID \
--logs '[
{"category": "AppServiceHTTPLogs", "enabled": true},
{"category": "AppServiceConsoleLogs", "enabled": true},
{"category": "AppServiceAppLogs", "enabled": true}
]' \
--metrics '[{"category": "AllMetrics", "enabled": true}]'
echo "Diagnostic settings configured"Summary Table β
| Feature | Configuration | Notes |
|---|---|---|
| App Service Plan | Standard S1 | Required for slots |
| Deployment Slots | Production + Staging | Max 5 on Standard |
| Auto-scaling | 2-10 instances | CPU-based rules |
| VNet Integration | Outbound connectivity | Delegated subnet |
| SSL | Managed certificate | Free with custom domain |
| Logging | Filesystem + Log Analytics | 35 days retention |
Exam Tips β
- Slot Swap: Zero-downtime deployment, swaps hostnames
- Sticky Settings: Use
--slot-settingsflag for environment-specific config - Scaling Tiers: Only Standard+ supports slots and auto-scale
- VNet Integration: Outbound only, requires subnet delegation
- Always On: Prevents cold starts, requires Basic+ tier
Cleanup β
bash
# Delete resource group
cd ..
rm -rf webapp-demo
az group delete --name $RG_NAME --yes --no-wait
echo "Cleanup initiated"Key Takeaways β
- Deployment Slots: Test before production, instant rollback
- Auto-scaling: Handle traffic spikes automatically
- VNet Integration: Secure access to backend resources
- Managed SSL: Free certificates for custom domains
- Diagnostics: Comprehensive logging to Log Analytics