Three-Tier Architecture on Azure
Build a segmented VNet with NSGs and VMs enforcing strict tier isolation.
Introduction
30 Second Summary
Every secure web application keeps its public-facing layer, business logic, and sensitive data in separate zones. If an attacker breaches the front door, they should never be able to walk straight to the vault.
In this project, you will build a three-tier network architecture on Azure using Virtual Machines, Virtual Networks, and Network Security Groups that enforce strict traffic isolation between tiers.
What You'll Build
A complete three-tier network architecture on Azure using Infrastructure as a Service components, with subnets and security rules that permit traffic only along the correct path.
By the end of this project, you'll have:
- A Virtual Network with three subnets representing web, application, and data tiers.
- Three Network Security Groups with priority-ordered rules enforcing strict tier-to-tier traffic flow.
- Three Linux VMs deployed into separate subnets with verified connectivity proving only permitted traffic flows between tiers.
- Secret Mission: Add an Azure Public Load Balancer in front of the web tier to distribute incoming HTTP traffic.
Are there any prerequisites?
You need an active Azure subscription and a Red Hat Linux environment with Bash. Basic familiarity with the Azure portal and the command line will help you move through the steps smoothly.
Set Up Your Environment
Before you can build a three-tier architecture, you need a working connection between your terminal and Azure.
In this step, you will verify your Azure CLI installation, authenticate to your Azure subscription, and create the resource group that will hold every resource in this project.
In this step, get ready to:
- Verify Azure CLI is installed and authenticate with az login.
- Set your default subscription and create a resource group.
- Confirm the resource group was created successfully.
Verify Azure CLI and authenticate
The Azure CLI is the command-line tool you will use to create and manage every Azure resource in this project. A quick version check confirms it is ready to use.
- Confirm Azure CLI is available by checking its version:
az --version
What should I see?
You should see output starting with azure-cli followed by a version number, plus a list of installed extensions. This confirms the CLI is installed and working.
Now you need to connect the CLI to your Azure account. The az login command opens an authentication flow so the CLI can manage resources on your behalf.
- Sign in to Azure by running:
az login
What happens when you run az login?
This command opens a browser window (or provides a device code) where you authenticate with your Azure credentials. After successful login, it displays a JSON list of your subscriptions.
The subscription marked isDefault: true is the one the CLI will use for all subsequent commands.
- Follow the browser prompts to complete authentication.
- After signing in, you should see JSON output listing your Azure subscriptions.
Browser not opening?
If you are working in a remote terminal or headless environment, az login may not be able to launch a browser. In that case, it will display a device code and a URL. Open the URL on any device, enter the code, and complete the sign-in there.
Help me troubleshoot az login on my Red Hat Linux machine.
Set your subscription and create the resource group
Azure resources live inside subscriptions. You need to tell the CLI which subscription to target before creating anything.
- Set your default subscription by running:
az account set --subscription [[AZURE_SUBSCRIPTION="your subscription name or ID"]]
What is a subscription?
An Azure subscription is a billing and access boundary for your cloud resources. If you only have one subscription, it is already selected by default after running az login.
If you have multiple subscriptions, this command ensures the CLI targets the correct one for the rest of the project.
Now you will create a resource group. A resource group is a logical container that holds related Azure resources so you can manage and delete them as a single unit.
- Create the resource group for this project by running:
az group create --name rg-threetier --location eastus
What does this command do?
This creates a resource group named rg-threetier in the eastus region. Every resource you build in this project will live inside this group.
The --location flag sets the region where the resource group metadata is stored. Your resources will also be deployed to this region.
- You should see JSON output that includes "provisioningState": "Succeeded". This confirms the resource group was created.
Seeing an authorization error?
Make sure you completed the az login step successfully. If your account does not have permission to create resource groups, contact your Azure administrator to get Contributor or Owner access on the subscription.
Help me fix an authorization error when creating a resource group.
Verify the resource group exists
A quick verification step ensures you are starting from a known-good state before building the network architecture.
- Confirm the resource group exists by running:
az group show --name rg-threetier
What should I see?
The output is a JSON object describing your resource group. Look for "provisioningState": "Succeeded" and "location": "eastus". These confirm the group is active and in the correct region.
- You should see "provisioningState": "Succeeded" in the output. Your resource group is ready.
Resource group not found?
Double check that you typed rg-threetier exactly as shown. Resource group names are case-sensitive. Also confirm you set the correct subscription with az account set in the previous substep.
Help me find my resource group in Azure CLI.
Your environment is ready. Next up, you will create the Virtual Network and three subnets that form the backbone of your three-tier architecture.
Create the Virtual Network and Subnets
Your resource group is ready. Now it's time to build the network foundation that all three tiers will live inside.
A three-tier architecture separates your application into web, application, and data layers. Each tier gets its own subnet within a single Virtual Network, creating isolated zones you can protect with security rules later.
In this step, get ready to:
- Create a Virtual Network with a /16 address space.
- Add three subnets for the web, app, and data tiers.
- Verify all three subnets exist with the correct address ranges.
Create the Virtual Network and web subnet
The az network vnet create command lets you create a VNet and its first subnet in one shot. You will set up the overall address space and carve out the web tier subnet at the same time.
- Create the Virtual Network and web subnet by running this command:
az network vnet create \
--name vnet-threetier \
--resource-group rg-threetier \
--address-prefixes 10.0.0.0/16 \
--subnet-name snet-web \
--subnet-prefixes 10.0.1.0/24
What does this command do?
- --name vnet-threetier names the Virtual Network that will hold all three tiers.
- --address-prefixes 10.0.0.0/16 reserves 65,536 IP addresses for the entire VNet. All subnets carve their addresses from this range.
- --subnet-name snet-web and --subnet-prefixes 10.0.1.0/24 create the web tier subnet with 256 addresses in the 10.0.1.x range.
- Wait for the command to complete. You should see a JSON response that includes "provisioningState": "Succeeded" for both the VNet and the subnet.
Why one VNet with multiple subnets?
Microsoft recommends placing each tier in its own subnet within a single VNet. Subnets act as security boundaries. In the next step, you will attach Network Security Groups to each subnet to control which tiers can talk to each other.
Seeing a permissions error?
Make sure you are signed into the correct subscription with az account show. If the wrong subscription is active, switch with az account set --subscription followed by your subscription name or ID.
Help me troubleshoot this permissions error
Add the application and data subnets
The VNet exists with one subnet. Now you need to add two more subnets for the application and data tiers using separate commands.
- Create the application subnet by running this command:
az network vnet subnet create \
--resource-group rg-threetier \
--vnet-name vnet-threetier \
--name snet-app \
--address-prefixes 10.0.2.0/24
What does this command do?
This adds a subnet named snet-app to the existing VNet. The 10.0.2.0/24 prefix gives the app tier its own 256-address range that does not overlap with the web tier.
- Create the data subnet by running this command:
az network vnet subnet create \
--resource-group rg-threetier \
--vnet-name vnet-threetier \
--name snet-data \
--address-prefixes 10.0.3.0/24
- Wait for both commands to return JSON with "provisioningState": "Succeeded".
Why do the subnets use /24 prefixes?
A /24 gives each tier 256 addresses (251 usable after Azure reserves 5). This is more than enough for a demonstration and keeps the address scheme clean. Each tier occupies a predictable block: 10.0.1.x for web, 10.0.2.x for app, 10.0.3.x for data.
Subnet creation failed?
Double-check that --vnet-name is spelled exactly vnet-threetier. A typo here creates an error because Azure cannot find the VNet to add the subnet to.
Help me fix this subnet creation error
Verify all three subnets exist
- List all subnets in the VNet by running this command:
az network vnet subnet list \
--resource-group rg-threetier \
--vnet-name vnet-threetier \
--output table
- Confirm the output shows three rows: snet-web with prefix 10.0.1.0/24, snet-app with prefix 10.0.2.0/24, and snet-data with prefix 10.0.3.0/24.
What should I see?
The table output displays each subnet name in one column and its address prefix in another. All three subnets should show Succeeded in the provisioning state column.
Only seeing one or two subnets?
Re-run the two az network vnet subnet create commands from the previous substep. If a command failed silently, you may have missed the error in your terminal scroll history.
Help me figure out why my subnets are missing
Your network foundation is in place. Three isolated subnets are ready and waiting for virtual machines. Next up, you will create Network Security Groups to control exactly which traffic is allowed between these tiers.
Create and Configure Network Security Groups
Your three-tier network now has a VNet with three subnets, but there is nothing stopping traffic from flowing freely between all of them. Without security rules, any resource in the web subnet could talk directly to the data subnet, which defeats the purpose of tier isolation.
Network Security Groups (NSGs) are Azure's built-in firewall for controlling traffic at the subnet or NIC level. In this step, you will create three NSGs with carefully ordered rules that enforce strict tier-to-tier communication.
In this step, get ready to:
- Create three NSGs and associate each with its corresponding subnet.
- Add allow and deny rules to enforce strict tier isolation.
- Verify that rule priority ordering is correct across all NSGs.
Create the NSGs and associate them with subnets
Each subnet in your three-tier architecture needs its own NSG to control which traffic can enter. You will create all three NSGs first, then attach each one to its matching subnet.
- Create the three Network Security Groups by running these commands:
az network nsg create --name nsg-web --resource-group rg-threetier
az network nsg create --name nsg-app --resource-group rg-threetier
az network nsg create --name nsg-data --resource-group rg-threetier
What do these commands do?
Each az network nsg create command creates an empty NSG resource inside your rg-threetier resource group.
At this point the NSGs have no custom rules and are not attached to anything. Azure's default rules still allow all VNet-to-VNet traffic, so isolation is not enforced yet.
- Associate each NSG with its corresponding subnet by running these commands:
az network vnet subnet update --resource-group rg-threetier --vnet-name vnet-threetier --name snet-web --network-security-group nsg-web
az network vnet subnet update --resource-group rg-threetier --vnet-name vnet-threetier --name snet-app --network-security-group nsg-app
az network vnet subnet update --resource-group rg-threetier --vnet-name vnet-threetier --name snet-data --network-security-group nsg-data
What does associating an NSG with a subnet do?
When an NSG is associated at the subnet level, its rules apply to every resource deployed into that subnet. This is different from associating an NSG at the NIC level, which only protects a single network interface.
Subnet-level association is the recommended approach for tier isolation because it ensures no resource in the subnet can bypass the rules.
- Verify the associations succeeded. Each command's JSON output includes a networkSecurityGroup field showing the attached NSG's resource ID.
Seeing a 'not found' error?
Double-check that the NSG names match exactly: nsg-web, nsg-app, nsg-data. Also confirm the subnet names are snet-web, snet-app, snet-data and the VNet is vnet-threetier.
help me troubleshoot my NSG subnet association error in Azure CLI
Add rules to the web and app tier NSGs
Azure evaluates NSG rules by priority number, where the lowest number has the highest priority. Once a matching rule is found, evaluation stops.
- Add a rule to nsg-web that allows HTTP traffic from the internet on port 80 by running:
az network nsg rule create --resource-group rg-threetier --nsg-name nsg-web --name allow-http --access Allow --protocol Tcp --direction Inbound --priority 100 --source-address-prefix Internet --source-port-range '*' --destination-address-prefix '*' --destination-port-range 80
What does this rule do?
This rule allows any TCP traffic from the internet to reach resources in the web subnet on port 80. The Internet service tag represents all public IP addresses.
Priority 100 means this rule is evaluated before Azure's default rules (which start at priority 65000). The web tier only needs one allow rule because Azure's default deny-from-internet rule already blocks everything else.
- Add two rules to nsg-app. The first allows traffic from the web subnet on port 8080. The second denies all other inbound traffic. Run these commands:
az network nsg rule create --resource-group rg-threetier --nsg-name nsg-app --name allow-web-to-app --access Allow --protocol Tcp --direction Inbound --priority 100 --source-address-prefix 10.0.1.0/24 --source-port-range '*' --destination-address-prefix '*' --destination-port-range 8080
az network nsg rule create --resource-group rg-threetier --nsg-name nsg-app --name deny-all-inbound --access Deny --protocol '*' --direction Inbound --priority 200 --source-address-prefix '*' --source-port-range '*' --destination-address-prefix '*' --destination-port-range '*'
Why do you need an explicit deny rule?
Azure's default rules include an AllowVNetInBound rule at priority 65000 that permits all traffic within the VNet. Without an explicit deny rule at a lower priority number (200), the app subnet would accept traffic from any subnet, not just the web tier.
The deny rule at priority 200 catches anything not already matched by the allow rule at priority 100. This enforces strict tier isolation.
- Confirm the commands succeeded. Each rule's JSON output shows the rule name, priority, and access decision.
Getting a protocol error?
Make sure you wrap the asterisk in single quotes ('*') for protocol, source-port-range, and destination-port-range on the deny rule. Without quotes, your shell may expand the asterisk as a glob pattern.
help me fix my NSG rule create command that is failing with a protocol error
Add rules to the data tier NSG and verify
The data tier is the most restricted. Only the app subnet should be able to reach it, and only on port 3306.
- Add the allow and deny rules to nsg-data by running these commands:
az network nsg rule create --resource-group rg-threetier --nsg-name nsg-data --name allow-app-to-data --access Allow --protocol Tcp --direction Inbound --priority 100 --source-address-prefix 10.0.2.0/24 --source-port-range '*' --destination-address-prefix '*' --destination-port-range 3306
az network nsg rule create --resource-group rg-threetier --nsg-name nsg-data --name deny-all-inbound --access Deny --protocol '*' --direction Inbound --priority 200 --source-address-prefix '*' --source-port-range '*' --destination-address-prefix '*' --destination-port-range '*'
What does this rule set enforce?
The allow-app-to-data rule permits only TCP traffic from 10.0.2.0/24 (the app subnet) on port 3306. The deny-all-inbound rule at priority 200 blocks everything else.
This means the web subnet (10.0.1.0/24) cannot reach the data tier directly. Traffic must flow through the app tier first.
- Verify the effective rules on nsg-app and nsg-data by running:
az network nsg rule list --resource-group rg-threetier --nsg-name nsg-app --output table
az network nsg rule list --resource-group rg-threetier --nsg-name nsg-data --output table
What should I see?
Each table should show your custom rules listed with their priority, access decision, and direction. For nsg-app, you should see allow-web-to-app at priority 100 and deny-all-inbound at priority 200.
For nsg-data, you should see allow-app-to-data at priority 100 and deny-all-inbound at priority 200. The lower priority number is always evaluated first.
Rules not showing in the table?
Make sure you specify --resource-group rg-threetier in the list command. Without it, Azure CLI cannot locate the NSG.
help me troubleshoot why az network nsg rule list is not showing my custom rules
Your three-tier architecture now has security boundaries in place. Next up, you will deploy a Linux VM into each subnet to bring the tiers to life and test whether the rules actually block unauthorized traffic.
Deploy Virtual Machines into Each Tier
Your network is locked down with NSGs enforcing strict tier isolation. The rules are in place, but without compute resources in each subnet, there is nothing to protect.
In this step, you will deploy three Linux Virtual Machines into the subnets you created. The web VM gets a public IP so users can reach it from the internet. The app and data VMs stay private, accessible only from within the VNet.
In this step, get ready to:
- Deploy the web tier VM with a public IP address.
- Deploy the app and data tier VMs with private-only access.
- Verify all three VMs are running in their respective subnets.
Deploy the web tier VM
The web tier is the public-facing entry point of your architecture. It needs a public IP address so external users can connect to it.
- Deploy the web tier VM by running this command:
az vm create \
--resource-group rg-threetier \
--name vm-web \
--image Ubuntu2204 \
--size Standard_B1s \
--vnet-name vnet-threetier \
--subnet snet-web \
--nsg "" \
--generate-ssh-keys
What does this command do?
This command creates a VM named vm-web in the snet-web subnet of your VNet.
- --image Ubuntu2204 selects Ubuntu 22.04 LTS as the operating system.
- --size Standard_B1s picks the smallest burstable VM (1 vCPU, 1 GiB RAM), keeping costs minimal.
- --nsg "" prevents Azure from auto-creating a NIC-level NSG. Your subnet-level NSG already handles security.
- --generate-ssh-keys creates an SSH key pair so you can connect to the VM later.
- Wait for the command to finish. It typically takes 1-2 minutes.
- In the JSON output, look for the publicIpAddress field. Note down this IP address. You will use it to SSH into the web VM in the next step.
VM creation failed?
- If you see a SKU availability error, the Standard_B1s size may not be available in your region. Run az vm list-skus --location eastus --size B1s --all --output table to check availability.
- If you see a quota error, your subscription may have reached its vCPU limit. Check your usage in the Azure portal under Subscriptions > Usage + quotas.
Help me troubleshoot my Azure VM creation error.
Deploy the app tier VM
The app tier sits behind the web tier. It does not need a public IP because only the web subnet is permitted to reach it.
- Deploy the app tier VM by running this command:
az vm create \
--resource-group rg-threetier \
--name vm-app \
--image Ubuntu2204 \
--size Standard_B1s \
--vnet-name vnet-threetier \
--subnet snet-app \
--public-ip-address "" \
--nsg "" \
--generate-ssh-keys
What changed for this VM?
The key difference is --public-ip-address "". This tells Azure to create the VM without any public IP. The VM is reachable only through its private IP within the VNet.
This matches the security pattern where backend services are never directly exposed to the internet.
- Wait for the command to complete. Note the privateIpAddress in the output. It will be in the 10.0.2.x range.
Seeing a timeout or slow provisioning?
Azure B1s VMs can take up to 2-3 minutes to provision. If the command appears stuck, let it run. If it fails after several minutes, re-run the same command.
Help me with a slow or failed Azure VM deployment.
Deploy the data tier VM and verify all VMs
The data tier is the most protected layer. Like the app tier, it has no public IP. Only the app subnet can send traffic to it.
- Deploy the data tier VM by running this command:
az vm create \
--resource-group rg-threetier \
--name vm-data \
--image Ubuntu2204 \
--size Standard_B1s \
--vnet-name vnet-threetier \
--subnet snet-data \
--public-ip-address "" \
--nsg "" \
--generate-ssh-keys
Why no NIC-level NSG on any VM?
By default, az vm create auto-creates a Network Security Group on the VM's network interface. Passing --nsg "" prevents this.
Azure evaluates NSG rules at both the subnet level and the NIC level. If you had two NSGs, you would need to manage rules in both places. Keeping all rules at the subnet level is cleaner and easier to audit.
- Wait for the command to complete. Note the privateIpAddress in the output. It will be in the 10.0.3.x range.
- Verify all three VMs are running by running this command:
az vm list --resource-group rg-threetier --output table
- You should see three VMs listed: vm-web, vm-app, and vm-data. All should show a Location of eastus.
How do I confirm only vm-web has a public IP?
The table output from az vm list does not show IP addresses directly. To check, look at the JSON output from each az vm create command you ran earlier. Only vm-web should have a publicIpAddress value. The other two should show an empty string or null for that field.
Missing a VM in the list?
- Double-check that you used --resource-group rg-threetier in all three az vm create commands. A VM created in the wrong resource group will not appear here.
- If a VM creation failed earlier, scroll up in your terminal to find the error and re-run that command.
Help me find my missing VM in the az vm list output.
All three VMs are deployed and running. Next up, you will SSH into the web VM and test that your NSG rules correctly permit and deny traffic between tiers.
Verify Tier-to-Tier Connectivity
You have a three-tier network architecture with NSG rules designed to enforce strict isolation. The real test is whether traffic actually flows only along the permitted paths.
In this step, you will prove that your Network Security Groups are working correctly by testing connectivity between tiers. You will confirm that the web tier can reach the app tier, the app tier can reach the data tier, and the web tier cannot bypass the app tier to reach data directly.
In this step, get ready to:
- SSH into the web tier VM and test connectivity to the app tier on port 8080.
- Verify that direct web-to-data traffic is blocked by the NSG deny rule.
- Confirm that app-to-data connectivity succeeds on port 3306.
SSH into vm-web and test web-to-app connectivity
The web tier VM is the only one with a public IP address, making it your entry point into the VNet. From inside the VNet, you can test whether the NSG rules permit or deny traffic between subnets.
- Retrieve the public IP address of vm-web and the private IP addresses of vm-app and vm-data by running this command:
az vm list-ip-addresses --resource-group rg-threetier --output table
What does this command do?
This lists the public and private IP addresses for all VMs in your resource group. You will see vm-web with a public IP (for SSH access) and a private IP in the 10.0.1.x range.
The vm-app VM will show a private IP in the 10.0.2.x range, and vm-data will show a private IP in the 10.0.3.x range. Note these down for the connectivity tests.
- Make a note of the public IP for vm-web, the private IP for vm-app, and the private IP for vm-data.
- SSH into vm-web using its public IP by running:
ssh [[VM_WEB_PUBLIC_IP="public IP address of vm-web"]]
What should I see?
You should see a welcome message from the Ubuntu server and a command prompt. If this is your first time connecting, you will be asked to confirm the host fingerprint. Type yes and press Enter to continue.
- From the vm-web shell, test connectivity to vm-app on port 8080 by running:
nc -zv [[VM_APP_PRIVATE_IP="private IP of vm-app (10.0.2.x)"]] 8080
What does this test prove?
The nc -zv command attempts a TCP connection to the specified host and port. The -z flag means scan only (don't send data) and -v gives verbose output.
If the connection succeeds, it proves that your nsg-app rule allow-web-to-app (priority 100, allowing inbound from 10.0.1.0/24 on port 8080) is working correctly.
- You should see output indicating the connection succeeded, such as a message containing succeeded or open.
Connection refused instead of succeeded?
A "Connection refused" message means the NSG allowed the traffic through, but no application is listening on port 8080 on vm-app. This is expected because you have not deployed an application on that port.
The key distinction is between "Connection refused" (NSG allowed it, no listener) and "Connection timed out" (NSG blocked it). A refused connection still proves the NSG rule is permitting the traffic.
If you see a timeout instead, double-check that your nsg-app has the allow-web-to-app rule with source 10.0.1.0/24 and destination port 8080.
Help me troubleshoot why my web-to-app connectivity test is timing out when it should be allowed by my NSG rules
Test that web-to-data traffic is blocked
The whole point of strict tier isolation is that the web tier cannot directly access the data tier. Your nsg-data only allows inbound from 10.0.2.0/24 (the app subnet), so traffic from 10.0.1.0/24 (the web subnet) should be denied.
- From the same vm-web shell, attempt to reach vm-data on port 3306 by running:
nc -zv -w 10 [[VM_DATA_PRIVATE_IP="private IP of vm-data (10.0.3.x)"]] 3306
What does the -w 10 flag do?
The -w 10 flag sets a 10-second timeout for the connection attempt. Without it, nc would wait for a very long time before giving up on a blocked connection.
Because the nsg-data deny-all-inbound rule (priority 200) blocks any traffic not from 10.0.2.0/24, this connection will time out after 10 seconds. A timeout proves the NSG is silently dropping the packets.
- Wait for the command to finish. You should see a timeout or failure message after approximately 10 seconds.
This confirms that web-to-data traffic is denied. Your strict tier isolation is working.
Connection succeeded when it should have been denied?
If the connection to vm-data succeeds from vm-web, your nsg-data deny rule may be misconfigured. Verify that the deny-all-inbound rule exists at priority 200 with source * and destination port *.
Also confirm that nsg-data is associated with snet-data by running az network vnet subnet show --resource-group rg-threetier --vnet-name vnet-threetier --name snet-data.
Help me figure out why my web-to-data traffic is not being blocked by my NSG
Verify app-to-data connectivity from vm-app
You cannot SSH from vm-web to vm-app because the NSG only allows port 8080, not port 22. Instead, you will use Azure's Run Command feature to execute a connectivity test directly on vm-app.
- Exit the SSH session on vm-web by running:
exit
- Back on your local machine, run a connectivity test on vm-app using Azure Run Command:
az vm run-command invoke \
--resource-group rg-threetier \
--name vm-app \
--command-id RunShellScript \
--scripts "nc -zv [[VM_DATA_PRIVATE_IP="private IP of vm-data (10.0.3.x)"]] 3306"
What is Run Command?
Azure Run Command lets you execute scripts on a VM remotely through the VM agent, without needing direct SSH access. This is perfect for vm-app because it has no public IP and the NSG blocks SSH from the web subnet.
The --command-id RunShellScript tells Azure to run a shell script on the Linux VM. The --scripts parameter contains the actual command to execute.
- Wait for the command to complete (this may take 30-60 seconds).
- In the JSON output, look for the message field under stdout. You should see output indicating the connection to port 3306 succeeded or was refused (both prove the NSG allowed the traffic through).
Why does connection refused still count as success?
You are testing NSG rules, not application availability. A "Connection refused" response means the packet reached vm-data and the OS rejected it because no service is listening on port 3306.
A timeout would mean the NSG blocked the packet before it ever reached the VM. Since you get a response (refused or open), the allow-app-to-data rule at priority 100 is doing its job.
Run Command timing out or returning an error?
If the Run Command itself fails, make sure vm-app is in the Running state by checking az vm list --resource-group rg-threetier --output table.
If the nc command inside Run Command shows a timeout, verify that nsg-data has the allow-app-to-data rule with source 10.0.2.0/24 and destination port 3306 at priority 100.
Help me troubleshoot why my app-to-data connectivity test is failing when it should be allowed
You have now verified all three connectivity scenarios that prove strict tier isolation is enforced.
Secret mission
Add a Public Load Balancer to the Web Tier
Your three-tier architecture isolates traffic between tiers. But in production, a single web VM behind a public IP is a single point of failure. Can you place an Azure Public Load Balancer in front of the web tier to distribute HTTP traffic and practice a key AZ-104 load balancing skill?
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project uses B-series VMs that incur compute charges while running, so cleaning up when you are done prevents unexpected costs.
Cost warning
Running VMs incur hourly compute charges even if you are not connected to them. Deallocated VMs stop compute billing but storage charges for managed disks remain. If you are finished with this project, delete the entire resource group to avoid any ongoing costs.
Resources you used:
- Resource group rg-threetier (contains all resources below).
- Virtual Network vnet-threetier with three subnets (snet-web, snet-app, snet-data).
- Three Network Security Groups (nsg-web, nsg-app, nsg-data).
- Three Virtual Machines (vm-web, vm-app, vm-data) with associated managed disks and NICs.
- Public IP addresses (one for vm-web, one for the load balancer pip-lb-web).
- Azure Load Balancer lb-web (if you completed the Secret Mission).
Keep everything running
No action needed. Choose this if you are still actively building or exploring the architecture.
- Your VMs will continue to incur compute charges while in the Running state.
- You can SSH back into vm-web at any time using its public IP to test connectivity again.
- To check current costs, open the Cost Management + Billing section in the Azure portal.
Pause - I'll come back to this later
Deallocate all VMs to stop compute billing while keeping your resources intact. Storage charges for managed disks will continue.
- Deallocate each VM by running these commands:
az vm deallocate --resource-group rg-threetier --name vm-web
az vm deallocate --resource-group rg-threetier --name vm-app
az vm deallocate --resource-group rg-threetier --name vm-data
- Verify all three VMs show a Stopped (deallocated) power state by running:
az vm list --resource-group rg-threetier --show-details --output table
- When you come back, start the VMs again with az vm start --resource-group rg-threetier --name <vm-name> for each VM.
Delete - I don't want to use this again
Remove all project resources permanently by deleting the resource group. This removes every resource inside it in one command.
- Delete the entire resource group and all its contents by running:
az group delete --name rg-threetier --yes --no-wait
- The --yes flag skips the confirmation prompt. The --no-wait flag returns control to your terminal immediately while deletion happens in the background.
- This removes the VNet, all three subnets, all three NSGs, all three VMs, their managed disks, NICs, public IPs, and the load balancer (if created).
- Verify the resource group is gone by running:
az group list --output table
- You should no longer see rg-threetier in the list. If it still appears with a Deleting state, wait a minute and run the command again.
Nice Work!
Nice Work!
You just built a production-pattern three-tier network architecture on Azure using Virtual Networks, Network Security Groups, and Virtual Machines. This is exactly the kind of infrastructure design tested on the AZ-104 exam.
You've learned how to:
- Created a Virtual Network with three isolated subnets representing web, application, and data tiers, enforcing segmentation with a 10.0.0.0/16 address space.
- Designed and applied Network Security Groups with priority-ordered rules that enforce strict tier-to-tier traffic flow, blocking unauthorized paths like web-to-data.
- Deployed three Linux VMs into separate subnets and verified connectivity to prove that only permitted traffic flows between tiers.
- Secret Mission: Added an Azure Public Load Balancer with health probes and backend pools to distribute HTTP traffic to the web tier.
Ready to quiz yourself?