DEV Community

Cover image for 3-Tier Architecture Deployment on Azure
Daniel Inyang
Daniel Inyang

Posted on

3-Tier Architecture Deployment on Azure

A complete step-by-step guide to deploying a production-grade Next.js + Node.js + MySQL stack on Azure

Architecture Overview

→ Internet → Azure Application Gateway (Web Tier)
→ Web VM (Next.js + Nginx) — Public Subnets
→ Internal Load Balancer (App Tier)
→ App VM (Node.js backend) — Private Subnets
→ Azure Database for MySQL (HA + Read Replica) — Private Subnets

Phase 1: The Foundation

Networking — Virtual Network, 6 Subnets, NAT Gateway, Route Tables

Step 1: Create a Resource Group

All Azure resources must live inside a Resource Group. This is your project container.

  1. Go to the Azure Portal (portal.azure.com) > search Resource Groups > Create.
  2. Subscription: Select your subscription.
  3. Resource group name: capstone-rg
  4. Region: East US (or your preferred region — choose ONE and use it consistently).
  5. Click Review + Create > Create.

Step 2: Create the Virtual Network (VNet)

This is the Azure equivalent of a VPC with a 10.0.0.0/16 address space.

  1. Search Virtual Networks > Create.
  2. Resource group: capstone-rg
  3. Name: capstone-vnet
  4. Region: East US
  5. Click Next: IP Addresses.
  6. Set IPv4 address space: 10.0.0.0/16
  7. Delete any default subnet that appears. We will add ours manually.
  8. Click Review + Create > Create.

Step 3: Create the 6 Subnets

Navigate to capstone-vnet > Subnets. Click + Subnet for each one below:

⚠️ Critical: The db-private-a and db-private-b subnets must be delegated to Microsoft.DBforMySQL/flexibleServers. When creating these subnets, under Subnet Delegation select Microsoft.DBforMySQL/flexibleServers. Do NOT attach an NSG to delegated subnets — Azure will block it.

Step 4: Create the NAT Gateway

This allows your private App and DB servers to download packages without direct internet exposure.

  1. Search NAT Gateway > Create.
  2. Resource group: capstone-rg | Name: capstone-nat | Region: East US.
  3. Under Outbound IP, click Create a new public IP address. Name: capstone-nat-ip.
  4. Click Next: Subnet. Associate the NAT gateway with: web-public-a and web-public-b.

💡 Note: Azure NAT Gateways associate at the subnet level, not a standalone ‘attachment’. By associating it with the public subnets, resources in private subnets that route through them will use this NAT.

  1. Click Review + Create > Create.

Step 5: Configure Route Tables (UDRs)

Azure automatically handles basic routing, but we need custom User-Defined Routes for private subnets to reach the NAT.

Public Route Table

  1. Search Route Tables > Create.
  2. Name: capstone-public-rt | Resource group: capstone-rg | Region: East US.
  3. After creation, go to the resource > Settings > Subnets > Associate. Add web-public-a and web-public-b.

💡 Note: Azure VNets have built-in internet routing for public subnets. No explicit 0.0.0.0/0 → Internet route is needed unless you have overriding rules.

Private Route Table

  1. Create another Route Table: capstone-private-rt.
  2. Go to Settings > Routes > Add. Add a route: Name: default-to-nat | Address prefix: 0.0.0.0/0 | Next hop type: Internet. (Azure NAT Gateway intercepts this and applies SNAT automatically).
  3. Go to Settings > Subnets > Associate. Add: app-private-a, app-private-b.

💡 Note: Do NOT associate the db subnets with this route table — they are delegated to MySQL and Azure manages their routing.

Phase 2: The Firewalls

Network Security Groups (NSGs) — Azure’s equivalent to AWS Security Groups
In Azure, NSGs can be attached to subnets or individual VM NICs. We follow the same strict chain as the AWS architecture:

• Internet → App Gateway NSG
• App Gateway NSG → Web VM NSG
• Web VM NSG → Internal LB NSG
• Internal LB NSG → App VM NSG
• App VM NSG → DB NSG

⚠️ Critical: Azure NSGs use Priority numbers (100–4096). Lower number = higher priority. Always assign priorities with gaps (e.g. 100, 200, 300) to leave room for future rules.

Step 1: Public App Gateway NSG

  1. Search Network Security Groups > Create.
  2. Name: capstone-appgw-nsg | Resource group: capstone-rg.
  3. After creation, go to Inbound security rules > Add: • Name: Allow-HTTP-Internet | Priority: 100 | Source: Any | Dest. Port: 80 | Protocol: TCP | Action: Allow • Name: Allow-HTTPS-Internet | Priority: 110 | Source: Any | Dest. Port: 443 | Protocol: TCP | Action: Allow • Name: Allow-AppGW-Infra | Priority: 120 | Source: GatewayManager | Dest. Port: 65200–65535 | Protocol: TCP | Action: Allow

⚠️ Critical: The GatewayManager rule on ports 65200–65535 is MANDATORY for Azure Application Gateway. Without it, the App Gateway health probes will fail and it will not provision correctly.

  1. Attach this NSG to web-public-a and web-public-b subnets.

Step 2: Web VM NSG

  1. Create NSG: capstone-web-vm-nsg
  2. Inbound rules: • Name: Allow-HTTP-FromAppGW | Priority: 100 | Source: (App Gateway subnet CIDR 10.0.1.0/24 & 10.0.2.0/24) | Dest. Port: 80 | Action: Allow • Name: Allow-SSH-MyIP | Priority: 200 | Source: Your IP address | Dest. Port: 22 | Protocol: TCP | Action: Allow
  3. Attach to web-public-a and web-public-b subnets.

Step 3: Internal Load Balancer NSG

  1. Create NSG: capstone-internal-lb-nsg
  2. Inbound rules: • Name: Allow-HTTP-FromWebVMs | Priority: 100 | Source: 10.0.1.0/24, 10.0.2.0/24 | Dest. Port: 80 | Action: Allow • Name: Allow-AzureLoadBalancer | Priority: 200 | Source: AzureLoadBalancer (service tag) | Dest. Port: * | Action: Allow
  3. Attach to app-private-a and app-private-b subnets.

Step 4: App VM NSG

  1. Create NSG: capstone-app-vm-nsg
  2. Inbound rules: • Name: Allow-3001-FromInternalLB | Priority: 100 | Source: 10.0.3.0/24, 10.0.4.0/24 | Dest. Port: 3001 | Action: Allow • Name: Allow-SSH-FromWebVM | Priority: 200 | Source: 10.0.1.0/24, 10.0.2.0/24 | Dest. Port: 22 | Protocol: TCP | Action: Allow • Name: Allow-AzureLoadBalancer | Priority: 300 | Source: AzureLoadBalancer | Dest. Port: * | Action: Allow
  3. Attach to app-private-a and app-private-b subnets.

Step 5: Database NSG

  1. Create NSG: capstone-db-nsg
  2. Inbound rules: • Name: Allow-MySQL-FromAppVM | Priority: 100 | Source: 10.0.3.0/24, 10.0.4.0/24 | Dest. Port: 3306 | Protocol: TCP | Action: Allow

💡 Note: For delegated MySQL subnets, Azure may manage some NSG rules automatically. If you encounter issues, verify the NSG is not conflicting with delegation requirements.

  1. Attach to db-private-a and db-private-b subnets.

Phase 3: The Data Layer

Azure Database for MySQL — Flexible Server (HA + Read Replica)
Azure Database for MySQL Flexible Server is the direct replacement for AWS RDS MySQL. It supports zone-redundant high availability (equivalent to Multi-AZ) and read replicas.

Step 1: Create the Primary MySQL Flexible Server (HA)

  1. Search Azure Database for MySQL flexible servers > Create.
  2. Resource group: capstone-rg
  3. Server name: capstone-db (This becomes part of your hostname: capstone-db.mysql.database.azure.com)
  4. Region: East US
  5. MySQL version: 8.0
  6. Compute + storage: Click Configure server. Select Burstable tier > B2ms (2 vCores, 8GB RAM). This is equivalent to db.t3.micro cost-tier.
  7. Under High Availability: Check Enable High Availability. Mode: Zone-redundant (equivalent to RDS Multi-AZ — deploys a standby in a separate Availability Zone).
  8. Standby availability zone: 2 (Primary will be in Zone 1).
  9. Admin username: admin
  10. Password: Password123! (or note whatever you use)
  11. Click Next: Networking. Networking Configuration
  12. Connectivity method: Private access (VNet Integration).
  13. Virtual network: capstone-vnet.
  14. Subnet: Select db-private-a (must be the delegated subnet).
  15. Private DNS zone: Create new. Name: capstone-db.private.mysql.database.azure.com.

⚠️ Critical: Private DNS zone is critical. Without it, your App VMs cannot resolve the MySQL hostname. Azure creates this automatically if you select ‘Create new’.

  1. Click Review + Create > Create. This takes 5–10 minutes.

Step 2: Create the Initial Database
Unlike AWS RDS which lets you set an initial DB name during creation, in Azure you must create the database after the server is provisioned.

  1. Once the server is Available, go to the resource > Databases (left menu) > Add.
  2. Database name: bookreview
  3. Click Save.

Step 3: Create the Read Replica

  1. Go to your capstone-db server > Settings > Replication > Add replica.
  2. Server name: capstone-db-replica
  3. Location: East US (same region for low latency).
  4. Click OK. Wait 5–10 minutes for it to become available.

💡 Note: Azure MySQL Flexible Server read replicas are asynchronous, same as AWS RDS read replicas. They are read-only and can be used for reporting/analytics queries.

Phase 4: The Business Layer

Internal Load Balancer + Node.js Backend VM
Step 1: Create the App VM (Backend)

  1. Search Virtual Machines > Create > Azure virtual machine.
  2. Resource group: capstone-rg
  3. Virtual machine name: capstone-app-vm
  4. Region: East US | Availability zone: Zone 1
  5. Image: Ubuntu Server 22.04 LTS
  6. Size: Click See all sizes. Choose Standard_B1ms (1 vCPU, 2GB) — equivalent to t2.micro.
  7. Authentication type: SSH public key.
  8. Username: azureuser
  9. SSH public key source: Generate new key pair. Name: capstone-key. Download the .pem file when prompted.
  10. Click Next: Disks > Next: Networking. Networking Settings
  11. Virtual network: capstone-vnet
  12. Subnet: app-private-a
  13. Public IP: None (CRITICAL — must be private only!)
  14. NIC network security group: None (our NSG is attached to the subnet already).
  15. Click Review + Create > Create.

Step 2: Create the Web VM (Jump Host + Frontend)

  1. Create another VM: capstone-web-vm
  2. Region: East US | Availability zone: Zone 1
  3. Image: Ubuntu Server 22.04 LTS | Size: Standard_B1ms
  4. SSH key: Use existing key (upload your capstone-key.pub).
  5. Networking: Virtual network: capstone-vnet | Subnet: web-public-a
  6. Public IP: Create new. Name: capstone-web-pip | SKU: Standard | Assignment: Static.
  7. NIC network security group: None.
  8. Click Review + Create > Create. ** Step 3: Connect and Configure the App VM (SSH Jump)** Because the App VM has no public IP, you must SSH into the Web VM first, then jump to the App VM. From your local machine — copy the key to the Web VM scp -i capstone-key.pem capstone-key.pem azureuser@:/home/azureuser/

NOTE: “azureuser” is my VMs username, so replace it with your VMs username

SSH into the Web VM
ssh -i capstone-key.pem azureuser@
From inside Web VM — jump to App VM
chmod 400 capstone-key.pem
ssh -i capstone-key.pem azureuser@

Step 4: Configure the Node.js Backend
Run the following commands inside the App VM:
Install Packages
sudo apt update && sudo apt install nodejs npm mysql-client -y
Clone and Configure the App
git clone https://github.com/pravinmishraaws/book-review-app.git
cd book-review-app/backend
npm install
sudo npm install -g pm2

Edit the Environment File
nano .env
Update these values:
• DB_HOST=capstone-db.mysql.database.azure.com
• DB_USER=admin
• DB_PASSWORD=Password123!
• DB_NAME=bookreview
• PORT=3001

💡 Note: The Azure MySQL hostname format is .mysql.database.azure.com — find it on the Azure Portal under your MySQL Flexible Server > Overview > Server name.

Seed and Start the Backend

node src/server.js

You should see ‘Server running on port 3001’ and ‘Database connected’. Press Ctrl+C to stop.
pm2 start src/server.js — name “backend”
pm2 save
pm2 startup
Copy and run the generated sudo command that pm2 startup outputs.
pm2 status

Step 5: Create the Internal Load Balancer
Create the Backend Pool

  1. Search Load Balancers > Create.
  2. Name: capstone-internal-lb | SKU: Standard | Type: Internal | Tier: Regional.
  3. Virtual network: capstone-vnet | Subnet: app-private-a.
  4. IP address assignment: Dynamic.
  5. Click Next: Backend Pools > Add a backend pool.
  6. Name: capstone-app-pool. Add capstone-app-vm by NIC.
  7. Click Next: Inbound Rules > Add a load balancing rule.
  8. Name: app-lb-rule | Frontend IP: (auto-assigned private IP) | Protocol: TCP | Port: 80 | Backend port: 3001.
  9. Health probe: Create new. Name: app-health-probe | Protocol: HTTP | Port: 3001 | Path: /
  10. Click Review + Create > Create.

💡 Note: Note the Internal LB’s private IP address from the Overview page. This is your equivalent to the Internal ALB DNS name.

Phase 5: The Presentation Layer
Web VM (Next.js + Nginx) + Azure Application Gateway

Step 1: Configure the Web VM (Frontend)

SSH back into your Web VM. Run these commands:
sudo apt update && sudo apt install nodejs npm nginx -y
sudo npm install -g pm2
git clone https://github.com/pravinmishraaws/book-review-app.git
cd book-review-app/frontend
npm install

Step 2: Create the Environment File
nano .env
Add this line:
NEXT_PUBLIC_API_URL=/api
Save and exit (Ctrl+O, Enter, Ctrl+X).

Step 3: Build and Start Next.js
npm run build
pm2 start npm — name “frontend” start
pm2 save && pm2 startup
Run the generated sudo command from pm2 startup.

Step 4: Configure Nginx
sudo nano /etc/nginx/sites-available/default

Delete everything and paste this configuration (replace the proxy_pass address with your Internal LB private IP):

server {
listen 80;
server_name _;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection ‘upgrade’;
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
location /api/ {
rewrite ^/api/(.*) /$1 break;
proxy_pass http://;
proxy_http_version 1.1;
proxy_set_header Host $host;
}
}

Save and exit (Ctrl+O, Enter, Ctrl+X)

Then,
sudo nginx -t
sudo systemctl restart nginx

Step 5: Create the Azure Application Gateway (Public ALB)
Azure Application Gateway is the equivalent of AWS’s Public Application Load Balancer. It operates at Layer 7.

  1. Search Application Gateway > Create.
  2. Name: capstone-appgw | Region: East US | Tier: Standard V2.
  3. Autoscaling: Minimum 1, Maximum 2 (cost saving).
  4. Virtual network: capstone-vnet | Subnet: web-public-a.
  5. Click Next: Frontends.
  6. Frontend IP type: Public. Create new public IP: capstone-appgw-ip.
  7. Click Next: Backends. Add a backend pool.
  8. Name: capstone-web-pool. Add capstone-web-vm (by NIC or IP).
  9. Click Next: Configuration. Add a routing rule.
  10. Rule name: http-rule | Priority: 100.
  11. Listener: Name: http-listener | Frontend IP: Public | Protocol: HTTP | Port: 80.
  12. Backend targets: Target type: Backend pool | Backend pool: capstone-web-pool.
  13. HTTP settings: Create new. Name: web-http-settings | Protocol: HTTP | Port: 80.
  14. Click Review + Create > Create. This takes 5–10 minutes.

💡 Note: After creation, get the Application Gateway’s public IP from the Frontend IP configurations. This is what your users will browse to.

POSSIBLE PROBLEM YOU MIGHT ENCOUNTER IN STEP 5

I keep getting this error at step 5 “Subnet must only have application gateway”
This is a very common Azure Application Gateway gotcha.
Exact cause. The error is clear and the fix is straightforward. Here’s what’s happening and how to fix it:

The Problem
Azure Application Gateway requires a dedicated subnet — you cannot deploy any other resource in the Application Gateway subnet.

You are trying to deploy the Application Gateway into web-public-a, which already has your Web VM sitting in it. Azure is rejecting this because the subnet is not empty.

The Fix: Create a dedicated subnet for the Application Gateway
Go to capstone-vnet > Subnets > + Subnet and add a new one:

Name: appgw-subnet
CIDR: 10.0.7.0/24
NSG: capstone-appgw-nsg

Then go back to your Application Gateway creation and select appgw-subnet instead of web-public-a.

Step 6: Update CORS in the Backend

SSH back to the App VM (via Web VM jump), then update CORS:
cd ~/book-review-app/backend
nano .env
Add or update:
ALLOWED_ORIGINS=http://
pm2 restart backend

Open your browser and navigate to: http://
The Book Review App should load successfully!

QUICK TEST:

Restart one of the DBs and see if your application is still reachable
Restart the VM also to see if pm2 auto starts the application.

OPTIONAL
Phase 6: High Availability with VM Scale Sets

Equivalent to AWS Auto Scaling Groups (ASGs) with Launch Templates

Step 1: Create Golden VM Images
Before scaling, capture the perfectly configured VMs as reusable images.

  1. Go to capstone-app-vm > Click Capture (from the top menu).
  2. Share image to Azure Compute Gallery: No (use Managed Image for simplicity).
  3. Image name: capstone-app-golden-image.
  4. Check: Automatically delete this virtual machine after creating the image.
  5. Click Review + Create > Create. Wait for the image to be created.
  6. Repeat for capstone-web-vm. Image name: capstone-web-golden-image.

⚠️ Critical: Capturing an image deallocates and deletes the source VM. Make sure your backend is fully configured and tested before capturing.

Step 2: Create the App Tier VM Scale Set (VMSS)

  1. Search Virtual machine scale sets > Create.
  2. Resource group: capstone-rg | Name: capstone-app-vmss.
  3. Region: East US | Availability zones: 1, 2.
  4. Image: Click See all images > My images tab > Select capstone-app-golden-image.
  5. Size: Standard_B1ms.
  6. Authentication: SSH key > username: ubuntu > Use existing key.
  7. Scaling: Scaling mode: Autoscaling. Instances: Min 2, Max 4, Default 2.
  8. Click Next: Networking.
  9. Virtual network: capstone-vnet | NIC subnet: app-private-a.
  10. Load balancing: Select Load balancer > capstone-internal-lb > Backend pool: capstone-app-pool.
  11. Public inbound ports: None.
  12. Click Review + Create > Create.

Step 3: Create the Web Tier VM Scale Set (VMSS)

  1. Create another scale set: capstone-web-vmss.
  2. Image: capstone-web-golden-image.
  3. Availability zones: 1, 2.
  4. Scaling: Min 2, Max 4, Default 2.
  5. Networking: VNet: capstone-vnet | Subnet: web-public-a.
  6. Public IP per instance: Enabled (so each VM gets a public IP for the App Gateway health checks).
  7. Load balancing: Application Gateway > capstone-appgw > Backend pool: capstone-web-pool.
  8. Click Review + Create > Create.

Step 4: Chaos Test — Prove High Availability
Your architecture is now highly available. Prove it:

  1. Go to Virtual Machines. Manually stop or delete one of the VMSS instances.
  2. The VMSS will detect the missing instance and auto-launch a replacement using the golden image.
  3. pm2 will auto-start the application on the new VM.
  4. Wait 3–5 minutes, then refresh the Application Gateway URL. The app will still be running.

Phase 7: Cleanup — Destroy All Resources

Always delete resources after your lab to avoid unnecessary charges. Follow this order:

Step 1: Delete Scale Sets & VMs

  1. Go to Virtual machine scale sets > Delete capstone-app-vmss and capstone-web-vmss.
  2. Go to Virtual machines > Delete any remaining VMs.

Step 2: Delete Load Balancers & Application Gateway

  1. Search Application Gateway > Delete capstone-appgw.
  2. Search Load Balancers > Delete capstone-internal-lb.
  3. Go to Public IP addresses > Delete capstone-appgw-ip and capstone-web-pip and capstone-nat-ip.

Step 3: Delete MySQL Flexible Server

  1. Search Azure Database for MySQL flexible servers.
  2. Delete capstone-db-replica first.
  3. Delete capstone-db (primary). Confirm deletion — no final snapshot needed for lab cleanup.
  4. Delete the Private DNS Zone: capstone-db.private.mysql.database.azure.com.

Step 4: Delete NAT Gateway

  1. Search NAT Gateways > Delete capstone-nat.
  2. Delete capstone-nat-ip public IP address. Step 5: Delete the Resource Group (Nuclear Option)
  3. Go to Resource Groups > capstone-rg.
  4. Click Delete resource group.
  5. Type the resource group name to confirm > Delete.

⚠️ Critical: Deleting the resource group deletes ALL resources inside it in one sweep — VNet, subnets, NSGs, route tables, VMs, disks, and everything else. This is the fastest cleanup method.

Step 6: Delete VM Images & Key

  1. Search Images > Delete capstone-app-golden-image and capstone-web-golden-image.
  2. If you stored the SSH key in Azure Key Vault or SSH Keys resource, delete it.

And its a wrap

Thank you for the opportunity to share my little knowledge. I hope this was informative.

Comments are welcome.

Till next time, always stay positive 👍

Shout out to Pravin Mishra, Lead Co-Mentor: Praveen Pandey
🤝 Co-Mentors: Egwu Oko, Tanisha Borana, Ranbir Kaur

P.S. This post is part of the DevOps Micro Internship (DMI) Cohort-2 by Pravin Mishra. You can start your DevOps journey by joining this
Discord community ( https://lnkd.in/e4wTfknn ).

DMI #Azure #CloudComputing #DevOps #CloudEngineering #Architecture #NextJS #NodeJS #MySQL #LearningInPublic

Top comments (0)