Skip to content

Project 3: Multi-Tier Web Application ​

Overview ​

Deploy a highly available, secure multi-tier web application with load balancing, web application firewall, and Azure Bastion for secure management. This architecture is common for enterprise web applications.

Difficulty: Intermediate to Advanced
Duration: 4-6 hours
Cost: ~$100-150/month (VMs, Load Balancer, App Gateway)

Architecture Diagram ​

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                                 INTERNET                                         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                      β”‚
                                      β”‚ HTTPS (443)
                                      β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                        APPLICATION GATEWAY + WAF v2                              β”‚
β”‚                              (Public IP)                                         β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚  - SSL Termination        - URL-based routing      - Autoscaling          β”‚  β”‚
β”‚  β”‚  - WAF Protection         - Health probes          - Session affinity     β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                      β”‚
                                      β”‚ HTTP (80)
                                      β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                           VNet: vnet-webapp (10.0.0.0/16)                        β”‚
β”‚                                                                                  β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚               Subnet: snet-appgw (10.0.0.0/24) - App Gateway              β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚                                      β”‚                                           β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚           WEB TIER - Subnet: snet-web (10.0.1.0/24)                       β”‚  β”‚
β”‚  β”‚                                   β”‚                                        β”‚  β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚  β”‚
β”‚  β”‚  β”‚            INTERNAL LOAD BALANCER (10.0.1.100)                      β”‚  β”‚  β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚  β”‚
β”‚  β”‚                   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                       β”‚  β”‚
β”‚  β”‚                   β”‚               β”‚               β”‚                       β”‚  β”‚
β”‚  β”‚                   β–Ό               β–Ό               β–Ό                       β”‚  β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”             β”‚  β”‚
β”‚  β”‚  β”‚   vm-web-01     β”‚ β”‚   vm-web-02     β”‚ β”‚   vm-web-03     β”‚             β”‚  β”‚
β”‚  β”‚  β”‚   Zone 1        β”‚ β”‚   Zone 2        β”‚ β”‚   Zone 3        β”‚             β”‚  β”‚
β”‚  β”‚  β”‚   Nginx/IIS     β”‚ β”‚   Nginx/IIS     β”‚ β”‚   Nginx/IIS     β”‚             β”‚  β”‚
β”‚  β”‚  β”‚   10.0.1.4      β”‚ β”‚   10.0.1.5      β”‚ β”‚   10.0.1.6      β”‚             β”‚  β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜             β”‚  β”‚
β”‚  β”‚                                   β”‚                                        β”‚  β”‚
β”‚  β”‚                          NSG: nsg-web                                     β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚                                      β”‚                                           β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚           APP TIER - Subnet: snet-app (10.0.2.0/24)                       β”‚  β”‚
β”‚  β”‚                                   β”‚                                        β”‚  β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚  β”‚
β”‚  β”‚  β”‚            INTERNAL LOAD BALANCER (10.0.2.100)                      β”‚  β”‚  β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚  β”‚
β”‚  β”‚                   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                       β”‚  β”‚
β”‚  β”‚                   β”‚               β”‚               β”‚                       β”‚  β”‚
β”‚  β”‚                   β–Ό               β–Ό               β–Ό                       β”‚  β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”             β”‚  β”‚
β”‚  β”‚  β”‚   vm-app-01     β”‚ β”‚   vm-app-02     β”‚ β”‚   vm-app-03     β”‚             β”‚  β”‚
β”‚  β”‚  β”‚   Zone 1        β”‚ β”‚   Zone 2        β”‚ β”‚   Zone 3        β”‚             β”‚  β”‚
β”‚  β”‚  β”‚   App Server    β”‚ β”‚   App Server    β”‚ β”‚   App Server    β”‚             β”‚  β”‚
β”‚  β”‚  β”‚   10.0.2.4      β”‚ β”‚   10.0.2.5      β”‚ β”‚   10.0.2.6      β”‚             β”‚  β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜             β”‚  β”‚
β”‚  β”‚                                   β”‚                                        β”‚  β”‚
β”‚  β”‚                          NSG: nsg-app                                     β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚                                      β”‚                                           β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚          DATA TIER - Subnet: snet-data (10.0.3.0/24)                      β”‚  β”‚
β”‚  β”‚                                   β”‚                                        β”‚  β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                                  β”‚  β”‚
β”‚  β”‚  β”‚   vm-sql-01     β”‚ β”‚   vm-sql-02     β”‚                                  β”‚  β”‚
β”‚  β”‚  β”‚   Primary       β”‚ β”‚   Secondary     β”‚                                  β”‚  β”‚
β”‚  β”‚  β”‚   SQL Server    │◀─────▢│   SQL Server    β”‚  Always On AG               β”‚  β”‚
β”‚  β”‚  β”‚   10.0.3.4      β”‚ β”‚   10.0.3.5      β”‚                                  β”‚  β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                  β”‚  β”‚
β”‚  β”‚                          NSG: nsg-data                                    β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚                                                                                  β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚          MANAGEMENT - Subnet: AzureBastionSubnet (10.0.4.0/27)            β”‚  β”‚
β”‚  β”‚                            Azure Bastion                                   β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

What You'll Learn ​

  • Deploy VMs across Availability Zones
  • Configure Azure Load Balancer (internal)
  • Deploy Application Gateway with WAF
  • Implement NSG rules for defense in depth
  • Configure Azure Bastion for secure management
  • Set up health probes and backend pools

Prerequisites ​

  • Azure subscription
  • Azure CLI installed
  • Basic understanding of load balancing concepts

Phase 1: Create Network Infrastructure ​

Step 1.1: Create Resource Group and VNet ​

bash
# Set variables
LOCATION="eastus"
RG_NAME="rg-webapp-lab-eastus"

# Create resource group
az group create \
  --name $RG_NAME \
  --location $LOCATION \
  --tags Project=MultiTierWebApp Environment=Lab

# Create VNet
az network vnet create \
  --resource-group $RG_NAME \
  --name vnet-webapp \
  --address-prefix 10.0.0.0/16 \
  --location $LOCATION

echo "VNet created"

Step 1.2: Create Subnets ​

bash
# Application Gateway subnet
az network vnet subnet create \
  --resource-group $RG_NAME \
  --vnet-name vnet-webapp \
  --name snet-appgw \
  --address-prefix 10.0.0.0/24

# Web tier subnet
az network vnet subnet create \
  --resource-group $RG_NAME \
  --vnet-name vnet-webapp \
  --name snet-web \
  --address-prefix 10.0.1.0/24

# App tier subnet
az network vnet subnet create \
  --resource-group $RG_NAME \
  --vnet-name vnet-webapp \
  --name snet-app \
  --address-prefix 10.0.2.0/24

# Data tier subnet
az network vnet subnet create \
  --resource-group $RG_NAME \
  --vnet-name vnet-webapp \
  --name snet-data \
  --address-prefix 10.0.3.0/24

# Azure Bastion subnet
az network vnet subnet create \
  --resource-group $RG_NAME \
  --vnet-name vnet-webapp \
  --name AzureBastionSubnet \
  --address-prefix 10.0.4.0/27

echo "All subnets created"

Phase 2: Create Network Security Groups ​

Step 2.1: Create Web Tier NSG ​

bash
# Create NSG
az network nsg create \
  --resource-group $RG_NAME \
  --name nsg-web \
  --location $LOCATION

# Allow HTTP from App Gateway subnet
az network nsg rule create \
  --resource-group $RG_NAME \
  --nsg-name nsg-web \
  --name AllowHTTPFromAppGW \
  --priority 100 \
  --direction Inbound \
  --access Allow \
  --protocol Tcp \
  --source-address-prefixes 10.0.0.0/24 \
  --destination-port-ranges 80 443

# Allow health probes from Azure
az network nsg rule create \
  --resource-group $RG_NAME \
  --nsg-name nsg-web \
  --name AllowAzureHealthProbe \
  --priority 110 \
  --direction Inbound \
  --access Allow \
  --protocol "*" \
  --source-address-prefixes AzureLoadBalancer \
  --destination-port-ranges "*"

# Allow RDP from Bastion
az network nsg rule create \
  --resource-group $RG_NAME \
  --nsg-name nsg-web \
  --name AllowRDPFromBastion \
  --priority 200 \
  --direction Inbound \
  --access Allow \
  --protocol Tcp \
  --source-address-prefixes 10.0.4.0/27 \
  --destination-port-ranges 3389 22

# Deny all other inbound
az network nsg rule create \
  --resource-group $RG_NAME \
  --nsg-name nsg-web \
  --name DenyAllInbound \
  --priority 4096 \
  --direction Inbound \
  --access Deny \
  --protocol "*" \
  --source-address-prefixes "*" \
  --destination-port-ranges "*"

# Associate NSG
az network vnet subnet update \
  --resource-group $RG_NAME \
  --vnet-name vnet-webapp \
  --name snet-web \
  --network-security-group nsg-web

Step 2.2: Create App Tier NSG ​

bash
# Create NSG
az network nsg create \
  --resource-group $RG_NAME \
  --name nsg-app \
  --location $LOCATION

# Allow from Web tier only
az network nsg rule create \
  --resource-group $RG_NAME \
  --nsg-name nsg-app \
  --name AllowFromWebTier \
  --priority 100 \
  --direction Inbound \
  --access Allow \
  --protocol Tcp \
  --source-address-prefixes 10.0.1.0/24 \
  --destination-port-ranges 8080 8443

# Allow health probes
az network nsg rule create \
  --resource-group $RG_NAME \
  --nsg-name nsg-app \
  --name AllowHealthProbe \
  --priority 110 \
  --direction Inbound \
  --access Allow \
  --protocol "*" \
  --source-address-prefixes AzureLoadBalancer \
  --destination-port-ranges "*"

# Allow RDP from Bastion
az network nsg rule create \
  --resource-group $RG_NAME \
  --nsg-name nsg-app \
  --name AllowRDPFromBastion \
  --priority 200 \
  --direction Inbound \
  --access Allow \
  --protocol Tcp \
  --source-address-prefixes 10.0.4.0/27 \
  --destination-port-ranges 3389 22

# Associate NSG
az network vnet subnet update \
  --resource-group $RG_NAME \
  --vnet-name vnet-webapp \
  --name snet-app \
  --network-security-group nsg-app

Step 2.3: Create Data Tier NSG ​

bash
# Create NSG
az network nsg create \
  --resource-group $RG_NAME \
  --name nsg-data \
  --location $LOCATION

# Allow SQL from App tier only
az network nsg rule create \
  --resource-group $RG_NAME \
  --nsg-name nsg-data \
  --name AllowSQLFromAppTier \
  --priority 100 \
  --direction Inbound \
  --access Allow \
  --protocol Tcp \
  --source-address-prefixes 10.0.2.0/24 \
  --destination-port-ranges 1433

# Allow RDP from Bastion
az network nsg rule create \
  --resource-group $RG_NAME \
  --nsg-name nsg-data \
  --name AllowRDPFromBastion \
  --priority 200 \
  --direction Inbound \
  --access Allow \
  --protocol Tcp \
  --source-address-prefixes 10.0.4.0/27 \
  --destination-port-ranges 3389

# Associate NSG
az network vnet subnet update \
  --resource-group $RG_NAME \
  --vnet-name vnet-webapp \
  --name snet-data \
  --network-security-group nsg-data

Phase 3: Deploy Web Tier VMs ​

Step 3.1: Create Web VMs Across Zones ​

bash
# Set admin credentials
ADMIN_USER="azureadmin"
ADMIN_PASSWORD="P@ssw0rd123!Complex"

# Create web VMs in different zones
for i in 1 2 3; do
  az vm create \
    --resource-group $RG_NAME \
    --name vm-web-0$i \
    --vnet-name vnet-webapp \
    --subnet snet-web \
    --image Win2022Datacenter \
    --size Standard_D2s_v3 \
    --admin-username $ADMIN_USER \
    --admin-password $ADMIN_PASSWORD \
    --zone $i \
    --public-ip-address "" \
    --no-wait
done

echo "Web VMs creation initiated across 3 zones"

Step 3.2: Install IIS on Web VMs ​

bash
# Wait for VMs to be ready
for i in 1 2 3; do
  az vm wait --resource-group $RG_NAME --name vm-web-0$i --created
done

# Install IIS using Custom Script Extension
for i in 1 2 3; do
  az vm extension set \
    --resource-group $RG_NAME \
    --vm-name vm-web-0$i \
    --name CustomScriptExtension \
    --publisher Microsoft.Compute \
    --settings '{"commandToExecute": "powershell -ExecutionPolicy Unrestricted Install-WindowsFeature -Name Web-Server -IncludeManagementTools; Add-Content -Path \"C:\\inetpub\\wwwroot\\Default.htm\" -Value \"Hello from vm-web-0'$i' in Zone '$i'\""}' \
    --no-wait
done

echo "IIS installation initiated"

Phase 4: Deploy App Tier VMs ​

Step 4.1: Create App VMs ​

bash
# Create app VMs in different zones
for i in 1 2 3; do
  az vm create \
    --resource-group $RG_NAME \
    --name vm-app-0$i \
    --vnet-name vnet-webapp \
    --subnet snet-app \
    --image Ubuntu2204 \
    --size Standard_D2s_v3 \
    --admin-username $ADMIN_USER \
    --generate-ssh-keys \
    --zone $i \
    --public-ip-address "" \
    --no-wait
done

echo "App VMs creation initiated"

Phase 5: Create Internal Load Balancer for App Tier ​

Step 5.1: Create Internal Load Balancer ​

bash
# Create Internal Load Balancer
az network lb create \
  --resource-group $RG_NAME \
  --name ilb-app \
  --sku Standard \
  --vnet-name vnet-webapp \
  --subnet snet-app \
  --frontend-ip-name FrontEndPool \
  --backend-pool-name BackEndPool \
  --private-ip-address 10.0.2.100

echo "Internal Load Balancer created"

Step 5.2: Create Health Probe ​

bash
# Create health probe
az network lb probe create \
  --resource-group $RG_NAME \
  --lb-name ilb-app \
  --name HealthProbe \
  --protocol Tcp \
  --port 8080 \
  --interval 5 \
  --threshold 2

Step 5.3: Create Load Balancing Rule ​

bash
# Create load balancing rule
az network lb rule create \
  --resource-group $RG_NAME \
  --lb-name ilb-app \
  --name HTTPRule \
  --protocol Tcp \
  --frontend-port 8080 \
  --backend-port 8080 \
  --frontend-ip-name FrontEndPool \
  --backend-pool-name BackEndPool \
  --probe-name HealthProbe \
  --idle-timeout 15 \
  --enable-tcp-reset true

Step 5.4: Add VMs to Backend Pool ​

bash
# Wait for App VMs
for i in 1 2 3; do
  az vm wait --resource-group $RG_NAME --name vm-app-0$i --created
done

# Get NIC IDs and add to backend pool
for i in 1 2 3; do
  NIC_ID=$(az vm show \
    --resource-group $RG_NAME \
    --name vm-app-0$i \
    --query "networkProfile.networkInterfaces[0].id" -o tsv)
  
  # Get IP config name
  IP_CONFIG=$(az network nic show --ids $NIC_ID --query "ipConfigurations[0].name" -o tsv)
  
  # Add to backend pool
  az network nic ip-config address-pool add \
    --resource-group $RG_NAME \
    --nic-name $(basename $NIC_ID) \
    --ip-config-name $IP_CONFIG \
    --lb-name ilb-app \
    --address-pool BackEndPool
done

echo "App VMs added to load balancer backend pool"

Phase 6: Deploy Application Gateway with WAF ​

Step 6.1: Create Public IP for App Gateway ​

bash
# Create public IP
az network public-ip create \
  --resource-group $RG_NAME \
  --name pip-appgw \
  --sku Standard \
  --allocation-method Static \
  --location $LOCATION

# Get the public IP
APPGW_IP=$(az network public-ip show \
  --resource-group $RG_NAME \
  --name pip-appgw \
  --query ipAddress -o tsv)

echo "App Gateway Public IP: $APPGW_IP"

Step 6.2: Create Application Gateway ​

Long Running Command

Application Gateway deployment takes 15-20 minutes.

bash
# Wait for web VMs to be ready
for i in 1 2 3; do
  az vm wait --resource-group $RG_NAME --name vm-web-0$i --created
done

# Get web VM private IPs
WEB_IP1=$(az vm show -g $RG_NAME -n vm-web-01 --show-details --query privateIps -o tsv)
WEB_IP2=$(az vm show -g $RG_NAME -n vm-web-02 --show-details --query privateIps -o tsv)
WEB_IP3=$(az vm show -g $RG_NAME -n vm-web-03 --show-details --query privateIps -o tsv)

# Create Application Gateway with WAF
az network application-gateway create \
  --resource-group $RG_NAME \
  --name appgw-webapp \
  --location $LOCATION \
  --sku WAF_v2 \
  --capacity 2 \
  --vnet-name vnet-webapp \
  --subnet snet-appgw \
  --public-ip-address pip-appgw \
  --http-settings-port 80 \
  --http-settings-protocol Http \
  --frontend-port 80 \
  --servers $WEB_IP1 $WEB_IP2 $WEB_IP3 \
  --priority 100

echo "Application Gateway created"

Step 6.3: Enable WAF Policy ​

bash
# Create WAF policy
az network application-gateway waf-policy create \
  --resource-group $RG_NAME \
  --name waf-policy-webapp

# Configure WAF policy settings
az network application-gateway waf-policy policy-setting update \
  --resource-group $RG_NAME \
  --policy-name waf-policy-webapp \
  --mode Prevention \
  --state Enabled \
  --max-request-body-size-kb 128 \
  --file-upload-limit-mb 100

# Associate WAF policy with App Gateway
az network application-gateway update \
  --resource-group $RG_NAME \
  --name appgw-webapp \
  --waf-policy waf-policy-webapp

Step 6.4: Configure Health Probes ​

bash
# Create custom health probe
az network application-gateway probe create \
  --resource-group $RG_NAME \
  --gateway-name appgw-webapp \
  --name HealthProbe \
  --protocol Http \
  --host-name-from-http-settings true \
  --path "/" \
  --interval 30 \
  --timeout 30 \
  --threshold 3

# Update backend http settings to use probe
az network application-gateway http-settings update \
  --resource-group $RG_NAME \
  --gateway-name appgw-webapp \
  --name appGatewayBackendHttpSettings \
  --probe HealthProbe

Phase 7: Deploy Azure Bastion ​

bash
# Create public IP for Bastion
az network public-ip create \
  --resource-group $RG_NAME \
  --name pip-bastion \
  --sku Standard \
  --allocation-method Static

# Create Azure Bastion
az network bastion create \
  --resource-group $RG_NAME \
  --name bastion-webapp \
  --public-ip-address pip-bastion \
  --vnet-name vnet-webapp \
  --sku Basic \
  --location $LOCATION

echo "Azure Bastion deployed"

Phase 8: Configure Monitoring ​

Step 8.1: Enable Boot Diagnostics ​

bash
# Create storage account for diagnostics
STORAGE_NAME="diagwebapp$(date +%s | tail -c 8)"
az storage account create \
  --resource-group $RG_NAME \
  --name $STORAGE_NAME \
  --sku Standard_LRS \
  --location $LOCATION

# Enable boot diagnostics on all VMs
for vm in vm-web-01 vm-web-02 vm-web-03 vm-app-01 vm-app-02 vm-app-03; do
  az vm boot-diagnostics enable \
    --resource-group $RG_NAME \
    --name $vm \
    --storage $STORAGE_NAME
done

Step 8.2: Configure Azure Monitor Alerts ​

bash
# Create action group
az monitor action-group create \
  --resource-group $RG_NAME \
  --name ag-webapp-alerts \
  --short-name WebAppAlrt \
  --action email admin admin@contoso.com

# Create CPU alert for VMs
for vm in vm-web-01 vm-web-02 vm-web-03; do
  VM_ID=$(az vm show -g $RG_NAME -n $vm --query id -o tsv)
  
  az monitor metrics alert create \
    --resource-group $RG_NAME \
    --name "High-CPU-$vm" \
    --scopes $VM_ID \
    --condition "avg Percentage CPU > 80" \
    --window-size 5m \
    --evaluation-frequency 1m \
    --action ag-webapp-alerts \
    --severity 2
done

Phase 9: Testing and Verification ​

Step 9.1: Test Application Gateway ​

bash
# Get App Gateway public IP
APPGW_IP=$(az network public-ip show \
  --resource-group $RG_NAME \
  --name pip-appgw \
  --query ipAddress -o tsv)

# Test connectivity
curl http://$APPGW_IP

# Test multiple times to verify load balancing
for i in {1..10}; do
  curl -s http://$APPGW_IP
  echo ""
done

Step 9.2: Test High Availability ​

  1. Connect to vm-web-01 via Bastion
  2. Stop IIS service:
    powershell
    Stop-Service W3SVC
  3. Refresh the website - should still work (other VMs serve traffic)
  4. Verify in App Gateway backend health

Step 9.3: Verify WAF Protection ​

bash
# Test WAF with SQL injection attempt (should be blocked)
curl "http://$APPGW_IP/?id=1;DROP TABLE users--"

# Test with XSS attempt (should be blocked)
curl "http://$APPGW_IP/?q=<script>alert('xss')</script>"

Step 9.4: Check Backend Health ​

bash
# Check App Gateway backend health
az network application-gateway show-backend-health \
  --resource-group $RG_NAME \
  --name appgw-webapp \
  --output table

Architecture Summary ​

ComponentPurposeConfiguration
Application GatewayL7 load balancerWAF_v2, 2 instances
Web VMsWeb servers3 VMs across zones
Internal LBApp tier balancingStandard SKU
App VMsApplication servers3 VMs across zones
Azure BastionSecure managementBasic SKU
NSGsNetwork securityPer-subnet rules

Cost Optimization Tips ​

  1. Use Reserved Instances: 40-60% savings for 1-3 year commitment
  2. Right-size VMs: Start with B-series for dev/test
  3. Use Spot VMs: For non-critical workloads
  4. Schedule auto-shutdown: For dev/test environments
  5. Use App Gateway autoscaling: Pay only for what you use

Cleanup ​

bash
# Delete resource group and all resources
az group delete --name $RG_NAME --yes --no-wait

echo "Cleanup initiated"

Key Takeaways ​

  1. Defense in Depth: NSGs at each tier
  2. High Availability: VMs across availability zones
  3. WAF Protection: L7 security with OWASP rules
  4. Secure Management: No public IPs on VMs
  5. Load Balancing: Both L7 (App GW) and L4 (ILB)

Next Steps ​

  • Add SSL certificates to Application Gateway
  • Configure autoscaling for App Gateway
  • Implement Azure SQL instead of VM-based SQL
  • Add Azure Front Door for global load balancing
  • Configure Azure Monitor workbooks

Released under the MIT License.