Skip to content

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 ​

TierMax SlotsUse Case
Free/Shared0Development only
Basic0Small workloads
Standard5Production workloads
Premium20Enterprise workloads
Isolated20High 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 table

Step 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 tsv

Step 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 table

Phase 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 SNI

Phase 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.zip

Step 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 ​

FeatureConfigurationNotes
App Service PlanStandard S1Required for slots
Deployment SlotsProduction + StagingMax 5 on Standard
Auto-scaling2-10 instancesCPU-based rules
VNet IntegrationOutbound connectivityDelegated subnet
SSLManaged certificateFree with custom domain
LoggingFilesystem + Log Analytics35 days retention

Exam Tips ​

  1. Slot Swap: Zero-downtime deployment, swaps hostnames
  2. Sticky Settings: Use --slot-settings flag for environment-specific config
  3. Scaling Tiers: Only Standard+ supports slots and auto-scale
  4. VNet Integration: Outbound only, requires subnet delegation
  5. 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 ​

  1. Deployment Slots: Test before production, instant rollback
  2. Auto-scaling: Handle traffic spikes automatically
  3. VNet Integration: Secure access to backend resources
  4. Managed SSL: Free certificates for custom domains
  5. Diagnostics: Comprehensive logging to Log Analytics

Released under the MIT License.