My Azure Home Lab Journey: Building Public and Private Subnets with Two Virtual Machines
My Azure Home Lab Journey: Building a Simple Public and Private Azure Network
When I started learning Azure networking, one concept kept coming up again and again: public vs private access inside a virtual network.
It sounds simple in theory:
- A web server should be reachable publicly so users can access it.
- A database server should stay private so only internal systems can talk to it.
But that idea becomes much easier to understand when you actually build it.
In this tutorial, I will walk through a simple Azure lab where I deployed:
- one web VM in a publicly accessible subnet
- one database VM in a private subnet
- a public IP only for the web VM
- Network Security Groups (NSGs) to control traffic
This is a great beginner-friendly lab for students learning Azure Fundamentals, especially around:
- Azure Virtual Network
- subnets
- public vs private connectivity
- NICs
- NSGs
- Virtual Machines
The best part is that this lab demonstrates a very common real-world pattern: internet-facing application tier + private backend tier.
What I wanted to build
Before jumping into commands, here is the design goal.
Architecture idea
- Resource Group:
Shirish - Region:
australiaeast - Existing VNet:
lab-vnet1 - Existing Public Subnet:
lab-vnet1-subnet1 - Existing Private Subnet:
lab-vnet1-private - Web VM:
lab-webserver - DB VM:
lab-db-server
Intended behavior
- The web server gets a public IP, so it can be accessed from the internet.
- The database server gets no public IP, so it remains private.
- NSGs are used to decide what traffic is allowed.
This is an important cloud concept: being inside the same virtual network does not automatically mean “open to everything.” Azure gives us control over how traffic flows.
The CLI variables section
I started by defining variables so the script would be easier to read, reuse, and modify.
# --- Variables ---
RG="Shirish"
LOCATION="australiaeast"
# Existing network objects
VNET="lab-vnet1"
SUBNET_PUBLIC="lab-vnet1-subnet1"
SUBNET_PRIVATE="lab-vnet1-private" # assumed to already exist
# VM settings
IMAGE="Win2022Datacenter"
SIZE="Standard_B2as_v2"
ADMIN_USER="ggtestuser"
ADMIN_PASS='rand%dass123' # keep single-quoted because of %
# Names
VM_WEB="lab-webserver"
VM_DB="lab-db-server"
PIP_WEB="pip-lab-webserver"
NIC_WEB="nic-lab-webserver"
NIC_DB="nic-lab-database"
NSG_WEB="nsg-lab-webserver"
NSG_DB="nsg-lab-database"
What these variables represent
RG="Shirish"
This is the Resource Group name.
An Azure Resource Group is a logical container for related resources. In this lab, all the networking and VM resources are created inside the same group so they can be managed together.
LOCATION="australiaeast"
This is the Azure region where resources will be deployed.
Regions matter because they affect:
- latency
- pricing
- service availability
- compliance requirements
VNET, SUBNET_PUBLIC, SUBNET_PRIVATE
These point to networking components that were already created earlier.
- Virtual Network (VNet) is Azure’s private network in the cloud.
- Subnets divide the VNet into smaller network segments.
In this lab:
- one subnet is used for the public-facing server
- one subnet is used for the private backend server
IMAGE="Win2022Datacenter"
This tells Azure which operating system image to use when creating the VM.
Here, I selected Windows Server 2022 Datacenter.
SIZE="Standard_B2as_v2"
This defines the VM size.
VM size controls:
- CPU
- memory
- expected performance
- cost
For a home lab or student exercise, a small VM size is usually enough.
ADMIN_USER and ADMIN_PASS
These are the admin credentials used for the VM.
Since the password contains a %, it was kept inside single quotes in the variable definition so the shell does not interpret it unexpectedly.
Remaining names
These are simply the resource names for:
- virtual machines
- network interfaces
- public IP
- network security groups
Using variables makes scripts much easier to maintain, especially when repeating patterns across environments.
Step 1: Create NSGs
In Azure, a Network Security Group (NSG) is like a traffic filter for network access. It contains rules that allow or deny traffic based on:
- source
- destination port
- protocol
- direction (inbound or outbound)
I created two NSGs:
- one for the web server
- one for the database server
NSG for the web server
az network nsg create -g $RG -n $NSG_WEB -l $LOCATION
az network nsg rule create -g $RG --nsg-name $NSG_WEB -n Allow-RDP \
--priority 1000 --direction Inbound --access Allow --protocol Tcp \
--source-address-prefixes Internet --destination-port-ranges 3389
az network nsg rule create -g $RG --nsg-name $NSG_WEB -n Allow-HTTP \
--priority 1010 --direction Inbound --access Allow --protocol Tcp \
--source-address-prefixes Internet --destination-port-ranges 80
First command: create the NSG
az network nsg create -g $RG -n $NSG_WEB -l $LOCATION
This creates a Network Security Group named nsg-lab-webserver in the Shirish resource group.
What service is involved here?
This uses the Azure Virtual Network / Network Security Group service.
Why do we need it?
Because the web server is meant to be public, we want to explicitly allow certain traffic from the internet.
Second command: allow RDP from the internet
az network nsg rule create -g $RG --nsg-name $NSG_WEB -n Allow-RDP \
--priority 1000 --direction Inbound --access Allow --protocol Tcp \
--source-address-prefixes Internet --destination-port-ranges 3389
This creates a rule inside the web server NSG.
What each part means
az network nsg rule create
Create a new NSG rule.-g $RG
Resource group where the NSG exists.--nsg-name $NSG_WEB
Apply the rule to the web NSG.-n Allow-RDP
Rule name.--priority 1000
NSG rules are processed by priority. Lower numbers are checked first.--direction Inbound
This applies to incoming traffic.--access Allow
Traffic matching this rule is allowed.--protocol Tcp
The allowed traffic uses TCP.--source-address-prefixes Internet
The source is anywhere on the internet.--destination-port-ranges 3389
Allow traffic to port 3389, which is RDP for Windows Remote Desktop.
Why this matters
Since the web VM is a Windows server, RDP allows remote administration.
Third command: allow HTTP from the internet
az network nsg rule create -g $RG --nsg-name $NSG_WEB -n Allow-HTTP \
--priority 1010 --direction Inbound --access Allow --protocol Tcp \
--source-address-prefixes Internet --destination-port-ranges 80
This rule allows inbound HTTP traffic on port 80 from the internet.
Why this matters
If this VM is acting as a simple web server, users need to be able to reach it over HTTP.
NSG for the database server
az network nsg create -g $RG -n $NSG_DB -l $LOCATION
az network nsg rule create -g $RG --nsg-name $NSG_DB -n Allow-RDP \
--priority 1000 --direction Inbound --access Allow --protocol Tcp \
--source-address-prefixes VirtualNetwork --destination-port-ranges 3389
az network nsg rule create -g $RG --nsg-name $NSG_DB -n Allow-HTTP \
--priority 1010 --direction Inbound --access Allow --protocol Tcp \
--source-address-prefixes VirtualNetwork --destination-port-ranges 80
This part is where the private subnet concept becomes clearer.
Create the DB NSG
az network nsg create -g $RG -n $NSG_DB -l $LOCATION
This creates a second NSG for the database VM.
Allow RDP only from within the VNet
az network nsg rule create -g $RG --nsg-name $NSG_DB -n Allow-RDP \
--priority 1000 --direction Inbound --access Allow --protocol Tcp \
--source-address-prefixes VirtualNetwork --destination-port-ranges 3389
Here the key difference is:
--source-address-prefixes VirtualNetwork
Instead of allowing traffic from the Internet, this only allows traffic coming from resources inside the same Azure Virtual Network.
Why this is important
This is the heart of the private design.
The DB server:
- has no public IP
- only accepts RDP from inside the VNet
- cannot be directly reached from the public internet
So even if someone knows the VM exists, they cannot connect directly from outside.
Allow HTTP only from within the VNet
az network nsg rule create -g $RG --nsg-name $NSG_DB -n Allow-HTTP \
--priority 1010 --direction Inbound --access Allow --protocol Tcp \
--source-address-prefixes VirtualNetwork --destination-port-ranges 80
This rule allows HTTP traffic, but again only from inside the VNet.
In a more realistic app design, the backend may expose an application port or database port instead of HTTP. But for a learning lab, HTTP is a simple way to demonstrate internal-only communication.
Step 2: Create a Public IP for the web server only
az network public-ip create -g $RG -n $PIP_WEB -l $LOCATION --sku Standard --allocation-method static --version IPv4
This command creates a Public IP resource for the web server.
What service is involved?
This uses Azure Public IP Address service.
What each option means
az network public-ip create
Create a public IP resource.-g $RG
Resource group.-n $PIP_WEB
Public IP resource name.-l $LOCATION
Azure region.--sku Standard
Use the Standard SKU. This is the recommended modern option and has better security/feature behavior than Basic.--allocation-method static
The IP address stays fixed after allocation.--version IPv4
Create an IPv4 public IP.
Why do this only for the web server?
Because the web server is intended to be publicly accessible.
The database VM does not get a public IP, which is exactly what makes it private from an internet access perspective.
This is one of the most important beginner lessons in Azure networking:
A VM without a public IP is not directly reachable from the internet.
Step 3: Create NICs
A NIC in Azure is a Network Interface Card resource. It connects a VM to a subnet and can also attach things like:
- public IP
- NSG
- private IP configuration
In Azure, the NIC is an actual resource you can manage separately.
az network nic create -g $RG -n $NIC_WEB -l $LOCATION \
--vnet-name $VNET --subnet $SUBNET_PUBLIC \
--public-ip-address $PIP_WEB --network-security-group $NSG_WEB
az network nic create -g $RG -n $NIC_DB -l $LOCATION \
--vnet-name $VNET --subnet $SUBNET_PRIVATE \
--network-security-group $NSG_DB
NIC for the web server
az network nic create -g $RG -n $NIC_WEB -l $LOCATION \
--vnet-name $VNET --subnet $SUBNET_PUBLIC \
--public-ip-address $PIP_WEB --network-security-group $NSG_WEB
What this does
It creates a NIC for the web VM and connects it to:
- VNet:
lab-vnet1 - Subnet:
lab-vnet1-subnet1 - Public IP:
pip-lab-webserver - NSG:
nsg-lab-webserver
Why this matters
This single NIC configuration gives the web VM:
- a private IP inside the VNet
- a public IP for internet access
- security filtering via the web NSG
So the web VM can be reached publicly, but only on ports allowed by the NSG.
NIC for the DB server
az network nic create -g $RG -n $NIC_DB -l $LOCATION \
--vnet-name $VNET --subnet $SUBNET_PRIVATE \
--network-security-group $NSG_DB
What this does
It creates a NIC for the database VM and connects it to:
- VNet:
lab-vnet1 - Subnet:
lab-vnet1-private - NSG:
nsg-lab-database
Notice what is missing:
- no public IP is attached
Why this matters
This keeps the DB VM private.
It still gets a private IP address inside the VNet, so other internal Azure resources can communicate with it. But it cannot be directly reached from the public internet.
This is exactly the pattern we usually want for backend systems.
Step 4: Create the virtual machines
Now that the NICs are ready, the VMs can be created on top of them.
az vm create -g $RG -n $VM_WEB --nics $NIC_WEB --image $IMAGE --size $SIZE \
--admin-username $ADMIN_USER --admin-password "$ADMIN_PASS" --authentication-type password
az vm create -g $RG -n $VM_DB --nics $NIC_DB --image $IMAGE --size $SIZE \
--admin-username $ADMIN_USER --admin-password "$ADMIN_PASS" --authentication-type password
Create the web VM
az vm create -g $RG -n $VM_WEB --nics $NIC_WEB --image $IMAGE --size $SIZE \
--admin-username $ADMIN_USER --admin-password "$ADMIN_PASS" --authentication-type password
What service is involved?
This uses the Azure Virtual Machines service.
What each option means
az vm create
Create a virtual machine.-g $RG
Resource group.-n $VM_WEB
VM name.--nics $NIC_WEB
Attach the existing NIC we created earlier.--image $IMAGE
Use the Windows Server 2022 image.--size $SIZE
VM size.--admin-username $ADMIN_USER
Admin username.--admin-password "$ADMIN_PASS"
Admin password.--authentication-type password
Use password authentication.
Why attach a NIC explicitly?
Because the NIC already contains the network design we want:
- correct subnet
- correct NSG
- public IP association
This makes the VM creation cleaner and more controlled.
Create the DB VM
az vm create -g $RG -n $VM_DB --nics $NIC_DB --image $IMAGE --size $SIZE \
--admin-username $ADMIN_USER --admin-password "$ADMIN_PASS" --authentication-type password
This creates the second VM using the DB NIC.
Since the DB NIC has:
- private subnet attachment
- private-only design
- DB-specific NSG
- no public IP
the VM automatically inherits that private network posture.
This is a powerful lesson for students: the VM’s exposure is heavily shaped by its NIC, IP assignment, subnet placement, and NSG rules.
Step 5: Output the public IP of the web server
az network public-ip show -g $RG -n $PIP_WEB --query ipAddress -o tsv
After deployment, this command displays the public IP address of the web VM.
What each part means
az network public-ip show
Display details about a public IP resource.-g $RG
Resource group.-n $PIP_WEB
Public IP resource name.--query ipAddress
Extract only theipAddressfield from the output.-o tsv
Return plain text instead of JSON.
Why this is useful
This gives a quick, copy-friendly public IP that can be used to:
- RDP into the web server
- browse to the web server in a browser
- verify public access works
What this lab demonstrates conceptually
This small lab may look simple, but it teaches several Azure cloud fundamentals very well.
1. A VNet is your private network in Azure
Think of it like your organization’s internal network, but hosted in Azure.
2. Subnets help separate workloads
Even inside the same VNet, resources can be placed into different subnets for better organization and control.
3. Public access depends on public IP assignment
A VM does not become internet-accessible just because it exists. It typically needs a public IP or another public entry point.
4. NSGs control allowed traffic
NSGs act like basic firewalls at the Azure network level.
5. Web tier and DB tier should not be exposed the same way
Frontend systems may need internet access. Backend systems usually should not.
This separation is a foundational idea in cloud architecture.
Why this is a good student lab
I like this lab because it is small enough to understand, but realistic enough to teach important design principles.
Students can clearly see:
- how Azure networking objects fit together
- how a public VM differs from a private VM
- how NSGs influence connectivity
- how a public IP changes accessibility
- how the same VNet can host both public-facing and internal workloads
It is much easier to remember these concepts after building them once.
A few practical notes about the script
There were some resources that already existed before running this script, especially:
- the resource group
- the virtual network
- the public subnet
- the private subnet
That means this script focused on the next layer of the journey:
- security
- interfaces
- public IP
- virtual machines
In a full end-to-end deployment, you might also create the VNet and subnets from scratch in the same script.
What I would improve even in a lab
Even for learning, there are a few things worth mentioning.
RDP open from the internet is convenient, but risky
Opening port 3389 to the internet is common in labs, but not recommended for serious environments.
HTTP on a DB VM is not typical in real life
For a demo, it is okay because it helps show internal-only traffic. But real database servers normally expose database-specific ports and only to trusted application tiers.
Password authentication is okay for labs
In production, stronger identity and secret-management practices are preferred.
What production environments usually add
For a home lab, test environment, or small project, this design is perfectly fine for learning. It is simple, visible, and helps explain the fundamentals clearly.
But in a real production environment, we usually add more Azure services around this scenario for security, availability, monitoring, and operational control.
Here are some important services that would commonly be incorporated.
1. Azure Bastion
Why we use it:
Instead of opening RDP (3389) directly to the internet, Azure Bastion allows secure RDP/SSH access through the Azure portal without exposing management ports publicly.
How it helps in this scenario:
The web VM and database VM could both be administered privately without leaving RDP open to Internet.
2. Azure Load Balancer or Application Gateway
Why we use it:
A single VM is not highly available. In production, web traffic is usually distributed across multiple servers.
How it helps in this scenario:
- Azure Load Balancer helps spread traffic across multiple web VMs.
- Azure Application Gateway adds Layer 7 features like host/path routing and can act as a web traffic entry point.
If the web tier grows beyond one VM, these services become very useful.
3. Azure Firewall
Why we use it:
NSGs are good for basic traffic filtering, but Azure Firewall gives centralized, advanced traffic control for large or sensitive environments.
How it helps in this scenario:
It can enforce network rules between tiers, control outbound traffic, and provide better inspection than simple NSG rules.
4. Network Watcher
Why we use it:
Troubleshooting networking in Azure can be difficult without visibility tools.
How it helps in this scenario:
Network Watcher helps validate:
- connectivity
- NSG behavior
- routing
- packet paths
It is very useful when students or engineers ask, “Why can this VM talk to that VM, but not the other way around?”
5. Azure Key Vault
Why we use it:
Storing admin passwords directly in scripts is not ideal in production.
How it helps in this scenario:
Credentials, certificates, and secrets can be stored securely in Key Vault instead of hardcoding them into deployment scripts.
6. Microsoft Entra ID and RBAC
Why we use it:
Identity and access management should be controlled centrally.
How it helps in this scenario:
Administrators can be granted only the permissions they need, instead of sharing common local credentials across servers.
7. Azure Monitor and Log Analytics
Why we use it:
Production systems need observability.
How it helps in this scenario:
These services provide:
- metrics
- alerts
- logs
- performance monitoring
For example, you could monitor:
- VM CPU and memory
- failed logins
- network issues
- application availability
8. Azure Backup
Why we use it:
VMs and data should be recoverable after accidental deletion, corruption, or ransomware incidents.
How it helps in this scenario:
Both the web VM and DB VM could be backed up regularly.
9. Azure Site Recovery
Why we use it:
Production workloads need disaster recovery options.
How it helps in this scenario:
If the primary region has a major issue, workloads can be replicated to another region for recovery.
10. Azure SQL Database or managed database services
Why we use it:
In production, we often avoid running databases directly on self-managed VMs unless necessary.
How it helps in this scenario:
Instead of a DB VM, a managed service such as Azure SQL Database or Azure Database for PostgreSQL/MySQL may reduce operational overhead, improve patching, backups, scaling, and availability.
This is a big cloud design principle: use managed services where possible.
11. Availability Sets or Availability Zones
Why we use it:
One VM can fail. Production systems need redundancy.
How it helps in this scenario:
Multiple web VMs can be spread across fault domains or zones so the application stays online during failures or maintenance events.
12. Private Endpoints
Why we use it:
Even platform services can be accessed privately.
How it helps in this scenario:
If the backend database moves to a managed Azure service, Private Endpoint can keep the traffic private inside the Azure network instead of exposing public service endpoints.
Final thoughts
This lab was a great way to understand a very important Azure concept:
not every VM should be treated the same way on the network.
The public-facing web server and the private database server have different responsibilities, so they should have different connectivity models.
By building this lab, students can see how Azure services work together:
- Resource Group organizes resources
- VNet provides private cloud networking
- Subnets separate workloads
- NSGs control traffic
- Public IP enables internet reachability
- NICs connect VMs to the network
- VMs provide the compute layer
For learning, this is a strong foundational exercise.
For production, we build on top of this with stronger security, better monitoring, higher availability, and more managed services.
And honestly, that is one of the best ways to learn cloud:
start with a simple working design, understand every piece, and then gradually layer in real-world improvements.
Full CLI used in this lab
# --- Variables ---
RG="Shirish"
LOCATION="australiaeast"
# Existing network objects
VNET="lab-vnet1"
SUBNET_PUBLIC="lab-vnet1-subnet1"
SUBNET_PRIVATE="lab-vnet1-private" # assumed to already exist
# VM settings
IMAGE="Win2022Datacenter"
SIZE="Standard_B2as_v2"
ADMIN_USER="ggtestuser"
ADMIN_PASS='rand%dass123' # keep single-quoted because of %
# Names
VM_WEB="lab-webserver"
VM_DB="lab-db-server"
PIP_WEB="pip-lab-webserver"
NIC_WEB="nic-lab-webserver"
NIC_DB="nic-lab-database"
NSG_WEB="nsg-lab-webserver"
NSG_DB="nsg-lab-database"
# --- NSGs ---
# Webserver: allow RDP/HTTP from Internet
az network nsg create -g $RG -n $NSG_WEB -l $LOCATION
az network nsg rule create -g $RG --nsg-name $NSG_WEB -n Allow-RDP \
--priority 1000 --direction Inbound --access Allow --protocol Tcp \
--source-address-prefixes Internet --destination-port-ranges 3389
az network nsg rule create -g $RG --nsg-name $NSG_WEB -n Allow-HTTP \
--priority 1010 --direction Inbound --access Allow --protocol Tcp \
--source-address-prefixes Internet --destination-port-ranges 80
# DB server: allow RDP/HTTP only from within the VNet (no public IP)
az network nsg create -g $RG -n $NSG_DB -l $LOCATION
az network nsg rule create -g $RG --nsg-name $NSG_DB -n Allow-RDP \
--priority 1000 --direction Inbound --access Allow --protocol Tcp \
--source-address-prefixes VirtualNetwork --destination-port-ranges 3389
az network nsg rule create -g $RG --nsg-name $NSG_DB -n Allow-HTTP \
--priority 1010 --direction Inbound --access Allow --protocol Tcp \
--source-address-prefixes VirtualNetwork --destination-port-ranges 80
# --- Public IP for webserver only ---
az network public-ip create -g $RG -n $PIP_WEB -l $LOCATION --sku Standard --allocation-method static --version IPv4
# --- NICs ---
az network nic create -g $RG -n $NIC_WEB -l $LOCATION \
--vnet-name $VNET --subnet $SUBNET_PUBLIC \
--public-ip-address $PIP_WEB --network-security-group $NSG_WEB
az network nic create -g $RG -n $NIC_DB -l $LOCATION \
--vnet-name $VNET --subnet $SUBNET_PRIVATE \
--network-security-group $NSG_DB
# --- VMs ---
az vm create -g $RG -n $VM_WEB --nics $NIC_WEB --image $IMAGE --size $SIZE \
--admin-username $ADMIN_USER --admin-password "$ADMIN_PASS" --authentication-type password
az vm create -g $RG -n $VM_DB --nics $NIC_DB --image $IMAGE --size $SIZE \
--admin-username $ADMIN_USER --admin-password "$ADMIN_PASS" --authentication-type password
# --- Output webserver public IP ---
az network public-ip show -g $RG -n $PIP_WEB --query ipAddress -o tsv