Deploy a Secured VM in a Custom VNet
Deploy a Linux VM inside a custom Azure VNet locked down with NSG rules.
Introduction
30 Second Summary
Think about how your home has locks on doors, a fence around the yard, and rules about who can come in. Cloud infrastructure needs the same kind of protection.
In this project, you will deploy a Linux Virtual Machine inside a custom Virtual Network on Azure, lock it down with a Network Security Group, and script the entire deployment using the Azure CLI.
What You'll Build
A secured Azure infrastructure environment consisting of a custom Virtual Network with a subnet, a Network Security Group that only permits SSH inbound traffic, and a Linux Virtual Machine deployed into that network.
By the end of this project, you'll have:
- A resource group with proper governance tags, a custom Virtual Network, and a Network Security Group restricting traffic to SSH only.
- A B1s Ubuntu Linux VM deployed inside the secured network and accessible via SSH.
- A reusable Azure CLI deployment script committed to a GitHub repository that any employer can review or run to recreate your infrastructure.
- Secret Mission: Add a backend subnet with its own NSG that denies all public inbound traffic and only allows SSH from the frontend subnet.
Are there any prerequisites?
You need an Azure free tier account and a GitHub account. Both should already be set up from earlier projects.
An AZ-900 level understanding of cloud concepts (resource groups, regions, subscriptions) is helpful but not required to follow along.
Open Cloud Shell and Create a Resource Group
Every Azure resource lives inside a resource group. Think of it as a labeled folder that keeps related resources together and makes cleanup straightforward.
In this step, you will open Azure Cloud Shell, verify the Azure CLI is ready, and create your first resource group with proper governance tags.
In this step, get ready to:
- Open Azure Cloud Shell in Bash mode and verify the Azure CLI.
- Create a resource group with descriptive tags.
- Verify the resource group exists and inspect its details.
Open Azure Cloud Shell in Bash mode
Azure Cloud Shell is a browser-based, authenticated terminal built into the Azure Portal. It comes preconfigured with the Azure CLI, Git, and GitHub CLI so you don't need to install anything locally.
- Sign in to the Azure Portal at portal.azure.com.
- Click the Cloud Shell icon (it looks like >_) in the top navigation bar of the portal.
- If prompted to choose a shell, select Bash.
First time opening Cloud Shell?
Cloud Shell may ask you to create a storage account or continue without storage (ephemeral mode). For this project, either option works.
If you choose No storage account required, your session starts faster but files are deleted when you close the window. If you choose to mount storage, your files persist between sessions.
- Verify the Azure CLI is available by running this command:
az version
You should see output showing the Azure CLI version and its components.
Don't see a version number?
Make sure you selected Bash (not PowerShell) as your shell type. You can switch by clicking the shell selector dropdown at the top left of the Cloud Shell pane.
Help me troubleshoot Cloud Shell not recognizing the az command.
Create the resource group
A resource group gives your infrastructure a single name you can manage, tag, and delete as one unit. Tags help you track which project a resource belongs to and what environment it runs in.
- Create your resource group with governance tags by running this command:
az group create \
--name az104-secured-vm-rg \
--location eastus \
--tags project=az104-portfolio environment=learning
What does this command do?
az group create provisions a new resource group in Azure. The --name flag sets the group's identifier, and --location specifies the datacenter region.
The --tags flag attaches key-value metadata to the group. Tagging is a governance best practice that makes it easy to find, cost-track, and clean up related resources later.
You should see a JSON response with "provisioningState": "Succeeded" confirming the group was created.
Seeing a permissions error?
Make sure your Azure free tier account has an active subscription. Run az account show to confirm your subscription is selected. If no subscription appears, you may need to complete the free account setup at portal.azure.com first.
Help me fix a permissions error when creating a resource group in Azure CLI.
Verify the resource group
- Confirm your resource group exists by running:
az group show --name az104-secured-vm-rg
What should I see?
The output is a JSON object showing the resource group's name, location, tags, and provisioningState. Confirm "name" is az104-secured-vm-rg and that both tags (project=az104-portfolio and environment=learning) appear in the tags section.
Now that you have verified the resource group, save these commands to a deployment script so you can replay the entire setup later.
- Open the Cloud Shell built-in editor with your new script file by running:
code deploy.sh
- Paste the following code into the editor. This is the beginning of your deployment script with variables at the top for easy customization:
#!/bin/bash
# ============================================
# AZ-104 Portfolio Project: Secured VM in Custom VNet
# ============================================
# Variables - customize these as needed
RESOURCE_GROUP="az104-secured-vm-rg"
LOCATION="eastus"
VNET_NAME="az104-vnet"
VNET_PREFIX="10.0.0.0/16"
SUBNET_NAME="web-subnet"
SUBNET_PREFIX="10.0.0.0/24"
NSG_NAME="web-nsg"
VM_NAME="az104-web-vm"
VM_IMAGE="Ubuntu2204"
VM_SIZE="Standard_B1s"
ADMIN_USER="azureuser"
# Step 1: Create Resource Group with tags
echo "Creating resource group..."
az group create \
--name $RESOURCE_GROUP \
--location $LOCATION \
--tags project=az104-portfolio environment=learning
Why start a script now?
Real cloud engineers use scripted deployments so any teammate can recreate the environment in seconds. By building deploy.sh incrementally, you will have a complete, runnable script by the end of the project.
The variables at the top let you change names, regions, or sizes in one place instead of hunting through every command.
- Save the file by pressing Ctrl+S.
- Close the editor by pressing Ctrl+Q.
- Confirm the file was saved by running:
ls
You should see deploy.sh listed in the output.
Don't see deploy.sh in the output?
Make sure you pressed Ctrl+S to save before closing the editor. You can reopen the file with code deploy.sh and try again.
Help me troubleshoot saving files in the Cloud Shell editor.
✔️ Awesome, I've got everything!
Great. Double check you have saved your file and that the resource group is confirmed with az group show.
ⓧ I'd like to double check the full code
Here is what deploy.sh should look like at this point:
#!/bin/bash
# ============================================
# AZ-104 Portfolio Project: Secured VM in Custom VNet
# ============================================
# Variables - customize these as needed
RESOURCE_GROUP="az104-secured-vm-rg"
LOCATION="eastus"
VNET_NAME="az104-vnet"
VNET_PREFIX="10.0.0.0/16"
SUBNET_NAME="web-subnet"
SUBNET_PREFIX="10.0.0.0/24"
NSG_NAME="web-nsg"
VM_NAME="az104-web-vm"
VM_IMAGE="Ubuntu2204"
VM_SIZE="Standard_B1s"
ADMIN_USER="azureuser"
# Step 1: Create Resource Group with tags
echo "Creating resource group..."
az group create \
--name $RESOURCE_GROUP \
--location $LOCATION \
--tags project=az104-portfolio environment=learning
Your resource group is live and tagged. Next up, you will build a Virtual Network inside it with a dedicated subnet for your web server.
Build the Virtual Network and Subnet
Your resource group is in place. Now you need a private network where your VM will live.
A Virtual Network (VNet) is an isolated network environment in Azure. Inside a VNet, you carve out smaller segments called subnets to organize resources. In this step, you will create both in a single command.
In this step, get ready to:
- Create a Virtual Network with a web subnet.
- Verify the VNet and subnet exist using Azure CLI commands.
Create the Virtual Network and subnet
Azure deploys resources into subnets, not directly into VNets. You need at least one subnet before you can attach any resource to the network.
- Create a Virtual Network named az104-vnet with a subnet named web-subnet by running this command in your Cloud Shell:
az network vnet create \
--resource-group az104-secured-vm-rg \
--name az104-vnet \
--address-prefixes 10.0.0.0/16 \
--subnet-name web-subnet \
--subnet-prefixes 10.0.0.0/24
What does this command do?
This single command does two things at once. It creates a VNet with the address space 10.0.0.0/16 (65,536 possible addresses) and carves out a subnet with the smaller range 10.0.0.0/24 (256 addresses).
Think of the VNet as a building and the subnet as a floor within it. Resources you deploy later will be placed on a specific floor.
After a few seconds, Cloud Shell returns a JSON block containing your new VNet's details, including its address space and the subnet.
Seeing an error about the resource group?
Make sure the resource group name matches exactly: az104-secured-vm-rg. If you used a different name in Step 1, substitute it here.
help me fix an error when creating my Virtual Network in Azure CLI
- Now add this command to your deployment script. In your Cloud Shell editor, open deploy.sh and add the following code at the bottom of the file:
# Step 2: Create Virtual Network with subnet
echo "Creating virtual network and subnet..."
az network vnet create \
--resource-group $RESOURCE_GROUP \
--name $VNET_NAME \
--address-prefixes $VNET_PREFIX \
--subnet-name $SUBNET_NAME \
--subnet-prefixes $SUBNET_PREFIX
Why use variables in the script?
The variables like $VNET_NAME and $SUBNET_PREFIX were defined at the top of your script in Step 1. Using variables means anyone who reuses your script can change values in one place without hunting through multiple commands.
- Save deploy.sh in the Cloud Shell editor.
✔️ Awesome, I've got everything!
Great. Make sure you have saved your file before moving on.
ⓧ I'd like to double check the full code
Here is the full deploy.sh file so far:
#!/bin/bash
# ============================================
# AZ-104 Portfolio Project: Secured VM in Custom VNet
# ============================================
# Variables - customize these as needed
RESOURCE_GROUP="az104-secured-vm-rg"
LOCATION="eastus"
VNET_NAME="az104-vnet"
VNET_PREFIX="10.0.0.0/16"
SUBNET_NAME="web-subnet"
SUBNET_PREFIX="10.0.0.0/24"
NSG_NAME="web-nsg"
VM_NAME="az104-web-vm"
VM_IMAGE="Ubuntu2204"
VM_SIZE="Standard_B1s"
ADMIN_USER="azureuser"
# Step 1: Create Resource Group with tags
echo "Creating resource group..."
az group create \
--name $RESOURCE_GROUP \
--location $LOCATION \
--tags project=az104-portfolio environment=learning
# Step 2: Create Virtual Network with subnet
echo "Creating virtual network and subnet..."
az network vnet create \
--resource-group $RESOURCE_GROUP \
--name $VNET_NAME \
--address-prefixes $VNET_PREFIX \
--subnet-name $SUBNET_NAME \
--subnet-prefixes $SUBNET_PREFIX
Verify the VNet and subnet
Running a create command does not guarantee everything landed correctly. Verification is a core habit for cloud administrators.
- Confirm the VNet details by running this command:
az network vnet show \
--resource-group az104-secured-vm-rg \
--name az104-vnet \
--output table
You should see a table row showing az104-vnet in the eastus location with the address prefix 10.0.0.0/16.
- Confirm the subnet exists inside your VNet by running:
az network vnet subnet list \
--resource-group az104-secured-vm-rg \
--vnet-name az104-vnet \
--output table
You should see one row with web-subnet and the address prefix 10.0.0.0/24.
Not seeing the subnet in the output?
Double check that your --vnet-name matches exactly: az104-vnet. A typo in the VNet name returns an empty result rather than an error.
help me verify my Virtual Network and subnet in Azure
Your network foundation is ready. Next up, you will create a Network Security Group to control which traffic is allowed into this subnet.
Create and Configure the Network Security Group
Your Virtual Network and subnet are in place, giving your future VM a private address space. But right now, that network accepts traffic from anywhere with no filtering at all.
A Network Security Group acts as a virtual firewall that filters inbound and outbound traffic based on rules you define. In this step, you'll create an NSG, add a rule to allow only SSH traffic, and attach it to your subnet.
In this step, get ready to:
- Create a Network Security Group.
- Add an inbound rule to allow SSH on port 22.
- Associate the NSG with your web subnet.
Create a Network Security Group
Before you can define any traffic rules, you need an NSG resource to hold them. Think of the NSG as an empty container that you'll fill with rules next.
- Create the NSG in your resource group by running this command in Cloud Shell:
az network nsg create \
--resource-group az104-secured-vm-rg \
--name web-nsg
What does this command do?
- az network nsg create provisions an empty Network Security Group resource inside your resource group.
- The NSG doesn't filter anything yet. You'll add rules to it in the next substep.
You should see a JSON response containing "provisioningState": "Succeeded" in the output.
Seeing a resource group not found error?
- Double check that your resource group name is exactly az104-secured-vm-rg. A single typo will cause the command to fail.
- Run az group list --output table to confirm the resource group exists.
Help me debug my az network nsg create command.
Add an inbound rule to allow SSH traffic
An NSG without rules relies on Azure's default rules, which deny all inbound traffic from the internet. You need an explicit allow rule so you can SSH into the VM you'll deploy later.
- Add an inbound rule named AllowSSH by running this command:
az network nsg rule create \
--resource-group az104-secured-vm-rg \
--nsg-name web-nsg \
--name AllowSSH \
--priority 100 \
--direction Inbound \
--access Allow \
--protocol Tcp \
--source-address-prefixes "*" \
--source-port-ranges "*" \
--destination-address-prefixes "*" \
--destination-port-ranges 22
What does this rule do?
- --priority 100 sets the rule's evaluation order. Lower numbers are processed first, and Azure's built-in deny rules sit at priority 65000+.
- --protocol Tcp and --destination-port-ranges 22 together restrict this rule to SSH traffic only.
- --source-address-prefixes "*" allows connections from any IP address. In production, you'd narrow this to your own IP.
You should see a JSON response with the rule details, including "access": "Allow" and "destinationPortRange": "22".
Seeing a priority conflict or invalid parameter?
- Make sure you typed --priority 100 not --priority 0. Priorities must be between 100 and 4096.
- Confirm that --nsg-name matches the NSG you just created (web-nsg).
Help me fix my NSG rule create command.
Associate the NSG with the web subnet
An NSG only filters traffic once it's attached to a subnet or a network interface. Right now your web-nsg exists but isn't protecting anything yet.
- Attach the NSG to your web-subnet by running this command:
az network vnet subnet update \
--resource-group az104-secured-vm-rg \
--vnet-name az104-vnet \
--name web-subnet \
--network-security-group web-nsg
Why attach to the subnet instead of the VM?
Attaching an NSG at the subnet level means every resource in that subnet is protected automatically. You don't need to configure each VM individually.
This is the recommended approach for consistent network policies across all resources in a subnet.
You should see a JSON response that includes "networkSecurityGroup" with the resource ID of web-nsg in the output.
Seeing a subnet not found error?
- Confirm you typed --name web-subnet and --vnet-name az104-vnet exactly. Subnet and VNet names are case-sensitive.
- Run az network vnet subnet list --resource-group az104-secured-vm-rg --vnet-name az104-vnet --output table to verify your subnet name.
Help me fix my subnet update command.
- Now add all three Step 3 commands to your deploy.sh file. Open the file in the Cloud Shell editor by running:
code deploy.sh
- Scroll to the bottom of your file (after the Step 2 VNet block) and add the following code:
# Step 3: Create Network Security Group
echo "Creating network security group..."
az network nsg create \
--resource-group $RESOURCE_GROUP \
--name $NSG_NAME
# Step 3b: Add SSH allow rule
echo "Adding SSH inbound rule..."
az network nsg rule create \
--resource-group $RESOURCE_GROUP \
--nsg-name $NSG_NAME \
--name AllowSSH \
--priority 100 \
--direction Inbound \
--access Allow \
--protocol Tcp \
--source-address-prefixes "*" \
--source-port-ranges "*" \
--destination-address-prefixes "*" \
--destination-port-ranges 22
# Step 3c: Associate NSG with subnet
echo "Associating NSG with subnet..."
az network vnet subnet update \
--resource-group $RESOURCE_GROUP \
--vnet-name $VNET_NAME \
--name $SUBNET_NAME \
--network-security-group $NSG_NAME
What's different about the script version?
The script uses variables like $RESOURCE_GROUP and $NSG_NAME instead of hardcoded values. These variables are already defined at the top of your file from earlier steps.
This makes the script reusable. Anyone can change the variables at the top and deploy the same infrastructure with different names.
- Save deploy.sh by pressing Ctrl+S (or Cmd+S on macOS).
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
Here is the complete deploy.sh file up to this point:
#!/bin/bash
# ============================================
# AZ-104 Portfolio Project: Secured VM in Custom VNet
# ============================================
# Variables - customize these as needed
RESOURCE_GROUP="az104-secured-vm-rg"
LOCATION="eastus"
VNET_NAME="az104-vnet"
VNET_PREFIX="10.0.0.0/16"
SUBNET_NAME="web-subnet"
SUBNET_PREFIX="10.0.0.0/24"
NSG_NAME="web-nsg"
VM_NAME="az104-web-vm"
VM_IMAGE="Ubuntu2204"
VM_SIZE="Standard_B1s"
ADMIN_USER="azureuser"
# Step 1: Create Resource Group with tags
echo "Creating resource group..."
az group create \
--name $RESOURCE_GROUP \
--location $LOCATION \
--tags project=az104-portfolio environment=learning
# Step 2: Create Virtual Network with subnet
echo "Creating virtual network and subnet..."
az network vnet create \
--resource-group $RESOURCE_GROUP \
--name $VNET_NAME \
--address-prefixes $VNET_PREFIX \
--subnet-name $SUBNET_NAME \
--subnet-prefixes $SUBNET_PREFIX
# Step 3: Create Network Security Group
echo "Creating network security group..."
az network nsg create \
--resource-group $RESOURCE_GROUP \
--name $NSG_NAME
# Step 3b: Add SSH allow rule
echo "Adding SSH inbound rule..."
az network nsg rule create \
--resource-group $RESOURCE_GROUP \
--nsg-name $NSG_NAME \
--name AllowSSH \
--priority 100 \
--direction Inbound \
--access Allow \
--protocol Tcp \
--source-address-prefixes "*" \
--source-port-ranges "*" \
--destination-address-prefixes "*" \
--destination-port-ranges 22
# Step 3c: Associate NSG with subnet
echo "Associating NSG with subnet..."
az network vnet subnet update \
--resource-group $RESOURCE_GROUP \
--vnet-name $VNET_NAME \
--name $SUBNET_NAME \
--network-security-group $NSG_NAME
Your network is now locked down. Only SSH traffic on port 22 can reach anything inside your web subnet. Next up, you'll deploy a Linux VM into this secured network and test that the SSH connection works.
Deploy the Virtual Machine
Your network infrastructure is fully locked down. The Virtual Network, subnet, and Network Security Group work together to ensure only SSH traffic on port 22 can reach your web-subnet.
Now it's time to deploy an actual Virtual Machine into that secured network. This is the compute resource your network infrastructure was built to protect.
In this step, get ready to:
- Deploy a B1s Ubuntu Linux VM into your secured subnet.
- Verify the VM is running and retrieve its public IP address.
- Test SSH connectivity to the new VM.
Deploy the VM into your secured subnet
The az vm create command provisions a new Virtual Machine and automatically creates supporting resources it needs (a network interface, OS disk, and public IP address).
- Deploy your VM by running this command in Cloud Shell:
az vm create \
--resource-group az104-secured-vm-rg \
--name az104-web-vm \
--image Ubuntu2204 \
--size Standard_B1s \
--vnet-name az104-vnet \
--subnet web-subnet \
--admin-username azureuser \
--generate-ssh-keys \
--output table
What does this command do?
- --image Ubuntu2204 selects Ubuntu 22.04 LTS as the operating system.
- --size Standard_B1s picks the smallest free-tier VM size (750 free hours/month for 12 months).
- --vnet-name and --subnet place the VM inside your existing secured network instead of creating a new one.
- --generate-ssh-keys creates an SSH key pair for you if one doesn't already exist in Cloud Shell.
- --output table formats the result as a readable table showing key details including the public IP address.
Why does this take a while?
Azure is provisioning a disk, a network interface, a public IP address, and the VM itself. This typically takes 1-3 minutes. Wait for the command to finish before moving on.
- Once the command completes, look at the output table. You should see a PublicIpAddress column with your VM's IP address (something like 20.x.x.x or 52.x.x.x).
- Copy that IP address. You will need it in the next substep.
Seeing an error about quotas or capacity?
Some Azure regions run out of capacity for certain VM sizes. If you see a message about quota or capacity, try changing the location to a different region (for example westus or westeurope). You would need to delete the resource group and re-create everything in the new region.
Help me troubleshoot my az vm create error
Test SSH connectivity
The whole point of your NSG configuration was to allow SSH access on port 22. Time to prove it works by connecting to your VM.
- Connect to your VM by running this command, replacing the IP address with the one from your output:
ssh azureuser@[[VM_PUBLIC_IP="your VM public IP address from the output above"]]
What should I see?
Cloud Shell may ask you to confirm the connection with a fingerprint message. Type yes and press Enter.
Once connected, your terminal prompt changes to something like azureuser@az104-web-vm. This confirms you are now inside your Ubuntu VM.
- Type exit and press Enter to disconnect from the VM and return to Cloud Shell.
Connection timed out?
A timeout means traffic isn't reaching port 22 on your VM. Double check that your NSG is associated with the subnet by running this command:
az network vnet subnet show --resource-group az104-secured-vm-rg --vnet-name az104-vnet --name web-subnet
Confirm the networkSecurityGroup field is not null. If it is, re-run the subnet update command from the previous step to associate web-nsg with web-subnet.
Help me fix my SSH connection timeout
Add the VM deployment to your script
Every command you've run so far needs to live in your deploy.sh script so the whole environment can be recreated with a single run.
- Open deploy.sh in the Cloud Shell editor by running:
code deploy.sh
- Scroll to the bottom of the file (after the NSG subnet association section).
- Add the following VM deployment section at the end of the file:
# Step 4: Deploy Virtual Machine
echo "Deploying virtual machine..."
az vm create \
--resource-group $RESOURCE_GROUP \
--name $VM_NAME \
--image $VM_IMAGE \
--size $VM_SIZE \
--vnet-name $VNET_NAME \
--subnet $SUBNET_NAME \
--admin-username $ADMIN_USER \
--generate-ssh-keys \
--output table
echo ""
echo "Deployment complete!"
echo "SSH into your VM with: ssh $ADMIN_USER@<PUBLIC_IP>"
echo ""
echo "To clean up all resources:"
echo "az group delete --name $RESOURCE_GROUP --yes --no-wait"
What does this code do?
This section uses the variables defined at the top of your script ($VM_NAME, $VM_IMAGE, $VM_SIZE) instead of hardcoded values. This makes the script reusable for different VM configurations.
The final echo statements print helpful instructions so anyone running the script knows how to connect and how to clean up afterwards.
- Save the file by pressing Ctrl+S.
- Close the editor by pressing Ctrl+Q.
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
Here is what your complete deploy.sh should look like at this point:
#!/bin/bash
# ============================================
# AZ-104 Portfolio Project: Secured VM in Custom VNet
# ============================================
# Variables - customize these as needed
RESOURCE_GROUP="az104-secured-vm-rg"
LOCATION="eastus"
VNET_NAME="az104-vnet"
VNET_PREFIX="10.0.0.0/16"
SUBNET_NAME="web-subnet"
SUBNET_PREFIX="10.0.0.0/24"
NSG_NAME="web-nsg"
VM_NAME="az104-web-vm"
VM_IMAGE="Ubuntu2204"
VM_SIZE="Standard_B1s"
ADMIN_USER="azureuser"
# Step 1: Create Resource Group with tags
echo "Creating resource group..."
az group create \
--name $RESOURCE_GROUP \
--location $LOCATION \
--tags project=az104-portfolio environment=learning
# Step 2: Create Virtual Network with subnet
echo "Creating virtual network and subnet..."
az network vnet create \
--resource-group $RESOURCE_GROUP \
--name $VNET_NAME \
--address-prefixes $VNET_PREFIX \
--subnet-name $SUBNET_NAME \
--subnet-prefixes $SUBNET_PREFIX
# Step 3: Create Network Security Group
echo "Creating network security group..."
az network nsg create \
--resource-group $RESOURCE_GROUP \
--name $NSG_NAME
# Step 3b: Add SSH allow rule
echo "Adding SSH inbound rule..."
az network nsg rule create \
--resource-group $RESOURCE_GROUP \
--nsg-name $NSG_NAME \
--name AllowSSH \
--priority 100 \
--direction Inbound \
--access Allow \
--protocol Tcp \
--source-address-prefixes "*" \
--source-port-ranges "*" \
--destination-address-prefixes "*" \
--destination-port-ranges 22
# Step 3c: Associate NSG with subnet
echo "Associating NSG with subnet..."
az network vnet subnet update \
--resource-group $RESOURCE_GROUP \
--vnet-name $VNET_NAME \
--name $SUBNET_NAME \
--network-security-group $NSG_NAME
# Step 4: Deploy Virtual Machine
echo "Deploying virtual machine..."
az vm create \
--resource-group $RESOURCE_GROUP \
--name $VM_NAME \
--image $VM_IMAGE \
--size $VM_SIZE \
--vnet-name $VNET_NAME \
--subnet $SUBNET_NAME \
--admin-username $ADMIN_USER \
--generate-ssh-keys \
--output table
echo ""
echo "Deployment complete!"
echo "SSH into your VM with: ssh $ADMIN_USER@<PUBLIC_IP>"
echo ""
echo "To clean up all resources:"
echo "az group delete --name $RESOURCE_GROUP --yes --no-wait"
File looks different from the example?
Make sure you added the new section after the existing NSG association block, not in the middle of it. The # Step 4: comment should come after the az network vnet subnet update command.
Help me fix my deploy.sh file
Your VM is live, secured, and accessible. In the next step, you'll finalize your script with a README and push everything to GitHub as a portfolio piece.
Create the Deployment Script and Push to GitHub
Your VM is live and you have verified SSH access. That proves the infrastructure works.
Now it is time to package everything into a reusable deployment script and push it to GitHub so any employer can review your work or recreate the environment in minutes.
In this step, get ready to:
- Create a deployment script with all Azure CLI commands organized for reuse.
- Write a README explaining the project and AZ-104 skills it demonstrates.
- Push both files to a new GitHub repository.
Create the deployment script
Real cloud engineers script their deployments so infrastructure can be recreated reliably. You will consolidate every command you ran in the previous steps into a single shell script with configurable variables at the top.
- Create and open deploy.sh in Cloud Shell's built-in editor by running this command:
code deploy.sh
What is the Cloud Shell editor?
Azure Cloud Shell includes a browser-based code editor built on the Monaco Editor (the same technology behind VS Code). Running code <filename> opens or creates a file directly in your browser.
- Paste the following script header and variables section into the editor:
#!/bin/bash
# ============================================
# AZ-104 Portfolio Project: Secured VM in Custom VNet
# ============================================
# This script deploys a secured Linux VM inside
# a custom Virtual Network with NSG rules.
# ============================================
# Variables - customize these as needed
RESOURCE_GROUP="az104-secured-vm-rg"
LOCATION="eastus"
VNET_NAME="az104-vnet"
VNET_PREFIX="10.0.0.0/16"
SUBNET_NAME="web-subnet"
SUBNET_PREFIX="10.0.0.0/24"
NSG_NAME="web-nsg"
VM_NAME="az104-web-vm"
VM_IMAGE="Ubuntu2204"
VM_SIZE="Standard_B1s"
ADMIN_USER="azureuser"
Why variables at the top?
Placing all configurable values in one section means anyone reusing the script can change the resource group name, location, or VM size without hunting through the entire file. This is a standard pattern in infrastructure scripting.
- Below the variables section, paste the command blocks that create all the resources:
# Step 1: Create Resource Group with tags
echo "Creating resource group..."
az group create \
--name $RESOURCE_GROUP \
--location $LOCATION \
--tags project=az104-portfolio environment=learning
# Step 2: Create Virtual Network with subnet
echo "Creating virtual network and subnet..."
az network vnet create \
--resource-group $RESOURCE_GROUP \
--name $VNET_NAME \
--address-prefixes $VNET_PREFIX \
--subnet-name $SUBNET_NAME \
--subnet-prefixes $SUBNET_PREFIX
# Step 3: Create Network Security Group
echo "Creating network security group..."
az network nsg create \
--resource-group $RESOURCE_GROUP \
--name $NSG_NAME
# Step 3b: Add SSH allow rule
echo "Adding SSH inbound rule..."
az network nsg rule create \
--resource-group $RESOURCE_GROUP \
--nsg-name $NSG_NAME \
--name AllowSSH \
--priority 100 \
--direction Inbound \
--access Allow \
--protocol Tcp \
--source-address-prefixes "*" \
--source-port-ranges "*" \
--destination-address-prefixes "*" \
--destination-port-ranges 22
# Step 3c: Associate NSG with subnet
echo "Associating NSG with subnet..."
az network vnet subnet update \
--resource-group $RESOURCE_GROUP \
--vnet-name $VNET_NAME \
--name $SUBNET_NAME \
--network-security-group $NSG_NAME
# Step 4: Deploy Virtual Machine
echo "Deploying virtual machine..."
az vm create \
--resource-group $RESOURCE_GROUP \
--name $VM_NAME \
--image $VM_IMAGE \
--size $VM_SIZE \
--vnet-name $VNET_NAME \
--subnet $SUBNET_NAME \
--admin-username $ADMIN_USER \
--generate-ssh-keys \
--output table
echo ""
echo "Deployment complete!"
echo "SSH into your VM with: ssh $ADMIN_USER@<PUBLIC_IP>"
echo ""
echo "To clean up all resources:"
echo "az group delete --name $RESOURCE_GROUP --yes --no-wait"
What does this script do?
These are the same commands you ran manually in Steps 1 through 4, now organized into a single executable file. The echo statements print progress messages so you can follow along when the script runs.
The final echo lines remind the user how to SSH in and how to clean up all resources afterwards.
- Save the file by pressing Ctrl+S.
- Close the editor by clicking the ... menu in the top right of the editor and selecting Close editor.
- Verify the file was created by running:
cat deploy.sh
You should see the full script contents printed in your terminal.
File is empty or missing?
If cat deploy.sh shows nothing or returns an error, reopen the file with code deploy.sh and paste the content again. Make sure you press Ctrl+S before closing the editor.
If the built-in editor is unresponsive, you can use nano deploy.sh as an alternative. Save with Ctrl+O and exit with Ctrl+X.
Help me troubleshoot my deploy.sh file in Cloud Shell
✔️ Awesome, I've got everything!
Great. Make sure you have saved deploy.sh and closed the editor before continuing.
ⓧ I'd like to double check the full code
#!/bin/bash
# ============================================
# AZ-104 Portfolio Project: Secured VM in Custom VNet
# ============================================
# This script deploys a secured Linux VM inside
# a custom Virtual Network with NSG rules.
# ============================================
# Variables - customize these as needed
RESOURCE_GROUP="az104-secured-vm-rg"
LOCATION="eastus"
VNET_NAME="az104-vnet"
VNET_PREFIX="10.0.0.0/16"
SUBNET_NAME="web-subnet"
SUBNET_PREFIX="10.0.0.0/24"
NSG_NAME="web-nsg"
VM_NAME="az104-web-vm"
VM_IMAGE="Ubuntu2204"
VM_SIZE="Standard_B1s"
ADMIN_USER="azureuser"
# Step 1: Create Resource Group with tags
echo "Creating resource group..."
az group create \
--name $RESOURCE_GROUP \
--location $LOCATION \
--tags project=az104-portfolio environment=learning
# Step 2: Create Virtual Network with subnet
echo "Creating virtual network and subnet..."
az network vnet create \
--resource-group $RESOURCE_GROUP \
--name $VNET_NAME \
--address-prefixes $VNET_PREFIX \
--subnet-name $SUBNET_NAME \
--subnet-prefixes $SUBNET_PREFIX
# Step 3: Create Network Security Group
echo "Creating network security group..."
az network nsg create \
--resource-group $RESOURCE_GROUP \
--name $NSG_NAME
# Step 3b: Add SSH allow rule
echo "Adding SSH inbound rule..."
az network nsg rule create \
--resource-group $RESOURCE_GROUP \
--nsg-name $NSG_NAME \
--name AllowSSH \
--priority 100 \
--direction Inbound \
--access Allow \
--protocol Tcp \
--source-address-prefixes "*" \
--source-port-ranges "*" \
--destination-address-prefixes "*" \
--destination-port-ranges 22
# Step 3c: Associate NSG with subnet
echo "Associating NSG with subnet..."
az network vnet subnet update \
--resource-group $RESOURCE_GROUP \
--vnet-name $VNET_NAME \
--name $SUBNET_NAME \
--network-security-group $NSG_NAME
# Step 4: Deploy Virtual Machine
echo "Deploying virtual machine..."
az vm create \
--resource-group $RESOURCE_GROUP \
--name $VM_NAME \
--image $VM_IMAGE \
--size $VM_SIZE \
--vnet-name $VNET_NAME \
--subnet $SUBNET_NAME \
--admin-username $ADMIN_USER \
--generate-ssh-keys \
--output table
echo ""
echo "Deployment complete!"
echo "SSH into your VM with: ssh $ADMIN_USER@<PUBLIC_IP>"
echo ""
echo "To clean up all resources:"
echo "az group delete --name $RESOURCE_GROUP --yes --no-wait"
Create the README file
A good README turns a script into a portfolio piece. It tells employers what the project demonstrates, how to run it, and which AZ-104 exam domains it covers.
- Open a new file for the README by running:
code README.md
- Paste the following content into the editor:
# AZ-104 Portfolio Project: Secured VM in Custom VNet
## What This Project Demonstrates
This project provisions a secured Azure infrastructure environment using the Azure CLI. It demonstrates hands-on skills from 3 of the 5 AZ-104 (Azure Administrator) exam domains:
| Exam Domain | Skills Demonstrated |
|---|---|
| Manage Azure identities and governance (20-25%) | Resource groups, tagging, naming conventions |
| Deploy and manage Azure compute resources (20-25%) | VM deployment, sizing, SSH key authentication |
| Implement and manage virtual networking (15-20%) | VNets, subnets, NSGs, inbound security rules |
## Architecture
```
Resource Group: az104-secured-vm-rg
|
+-- Virtual Network: az104-vnet (10.0.0.0/16)
|
+-- Subnet: web-subnet (10.0.0.0/24)
| NSG: web-nsg (Allow SSH only)
|
+-- VM: az104-web-vm (Ubuntu 22.04, Standard_B1s)
```
## Prerequisites
- Azure account (free tier works)
- Azure Cloud Shell (Bash) or Azure CLI installed locally
## How to Run
1. Open Azure Cloud Shell (Bash) from the Azure Portal
2. Clone this repository:
```bash
git clone https://github.com/YOUR_USERNAME/az104-secured-vm.git
cd az104-secured-vm
```
3. Make the script executable and run it:
```bash
chmod +x deploy.sh
./deploy.sh
```
4. SSH into your VM using the public IP from the output:
```bash
ssh azureuser@<PUBLIC_IP>
```
## Cleanup
To delete all resources and stop any charges:
```bash
az group delete --name az104-secured-vm-rg --yes --no-wait
```
## AZ-104 Exam Relevance
This project directly maps to skills tested on the Microsoft AZ-104: Azure Administrator Associate exam, specifically:
- Creating and managing resource groups with tags
- Deploying VMs with specific sizing and networking
- Configuring Virtual Networks and subnets
- Implementing Network Security Groups with custom rules
- Using Azure CLI for infrastructure automation
What makes this README effective?
The table maps your project directly to AZ-104 exam domains with percentage weights. This tells a hiring manager exactly which skills you are demonstrating.
The architecture diagram gives a quick visual overview. The "How to Run" section means anyone can clone and recreate your infrastructure in minutes.
- Save the file by pressing Ctrl+S.
- Close the editor by clicking ... and selecting Close editor.
- Confirm both files exist by running:
ls
You should see deploy.sh and README.md listed in the output.
Only see one file?
If only one file appears, reopen the missing file with code <filename> and paste its content again. Remember to save with Ctrl+S before closing.
Help me create my README.md in Cloud Shell
Initialize Git and push to GitHub
With both files ready, you will initialize a Git repository and push to GitHub using the preinstalled GitHub CLI. Cloud Shell has both git and gh preinstalled, so there is nothing extra to install.
- Initialize a Git repository and commit your files by running these commands:
git init -b main
git add .
git commit -m "Initial commit: AZ-104 secured VM deployment script"
What do these commands do?
- git init -b main creates a new Git repository with main as the default branch name.
- git add . stages both deploy.sh and README.md for commit.
- git commit saves a snapshot of your files with a descriptive message.
- Authenticate the GitHub CLI by running:
gh auth login
What will gh auth login ask?
The interactive prompts will ask you to:
- Select GitHub.com as the host.
- Choose HTTPS as the Git protocol.
- Confirm you want to authenticate Git with your GitHub credentials.
- Select Login with a web browser and follow the on-screen instructions to paste the one-time code into your browser.
- Follow the prompts to complete authentication.
- Once authenticated, create the GitHub repository and push your code by running:
gh repo create az104-secured-vm --public --source=. --push
What does this command do?
- gh repo create az104-secured-vm creates a new repository named az104-secured-vm on your GitHub account.
- --public makes the repository visible to anyone (important for a portfolio piece).
- --source=. tells GitHub CLI to use the current directory as the source.
- --push pushes your committed files to the new repository immediately.
You should see output confirming the repository was created and the push was successful, including a URL like https://github.com/YOUR_USERNAME/az104-secured-vm.
- Visit the repository URL shown in the output to confirm both files are visible on GitHub.
Authentication failed or push denied?
If you see a permission error, run gh auth status to check if you are authenticated. If not, run gh auth login again and make sure you complete the browser step.
If the repository name is already taken on your account, choose a different name (for example az104-secured-vm-project) in the gh repo create command.
Help me push my project to GitHub from Cloud Shell
Your deployment is now fully documented and shareable. Any employer can clone your repository, read the README, and run the script to see your AZ-104 skills in action.
Secret mission
Isolate a Backend Subnet
In production environments, backend services like databases should never be directly reachable from the internet. Can you add a second subnet with a deny-all rule that only permits SSH from the frontend subnet?
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.
The B1s VM is covered by Azure's 12-month free tier (750 hours/month). VNets, subnets, and NSGs are always free. If your free tier has expired, ongoing costs will apply.
Cost warning
If your Azure free tier has expired, the B1s VM costs approximately $7.59/month. The Public IP address also has a small hourly charge outside the free tier.
If you are no longer experimenting, choose Pause or Delete below to avoid charges.
Resources you used:
- 1 Resource Group (az104-secured-vm-rg).
- 1 Virtual Network (az104-vnet) with 2 subnets (free).
- 2 Network Security Groups (web-nsg and backend-nsg) (free).
- 1 Virtual Machine (az104-web-vm), Standard_B1s, covered by free tier for 12 months.
- 1 Public IP address and 1 OS managed disk (covered by free tier for 12 months).
- 1 GitHub repository (az104-secured-vm) (free).
Keep everything running
No action needed. Choose this if you are still actively experimenting with SSH access, testing NSG rules, or practicing Azure CLI commands.
- Your VM remains running and accessible via SSH.
- All resources stay within free tier limits as long as your 12-month free period is active.
- Your GitHub repository remains publicly available as a portfolio piece.
Pause - I'll come back to this later
Shut down the VM to stop compute charges while keeping your disk, network, and IP intact. You can restart later without rebuilding.
- Deallocate the VM by running this command in Cloud Shell:
az vm deallocate --resource-group az104-secured-vm-rg --name az104-web-vm
What does deallocate do?
Deallocating stops the VM and releases the compute resources so you are not charged for the VM itself. Your OS disk and network configuration remain intact.
To start it again later, run az vm start --resource-group az104-secured-vm-rg --name az104-web-vm.
- Your GitHub repository stays available. No action needed there.
Delete - I don't want to use this again
Remove all Azure resources and the GitHub repository. This is irreversible.
- Delete the entire resource group (this removes the VM, VNet, subnets, NSGs, public IP, and disk) by running:
az group delete --name az104-secured-vm-rg --yes --no-wait
What does --no-wait do?
The --no-wait flag returns control of your terminal immediately while Azure deletes resources in the background. Deletion typically takes 2-5 minutes.
- Delete the GitHub repository by going to your repository page on GitHub.
- Click the Settings tab in the top navigation bar.
- Scroll to the bottom and find the Danger Zone section.
- Click Delete this repository and follow the confirmation prompts.
Nice Work!
Nice Work!
You just deployed a fully secured Azure Virtual Machine inside a custom Virtual Network, locked it down with a Network Security Group, and packaged the whole thing as a reusable script on GitHub.
You've learned how to:
- Build a custom Virtual Network with subnets and configure a Network Security Group with explicit inbound rules to control traffic using Azure CLI.
- Deploy a Linux VM into a secured network and verify SSH connectivity.
- Package an entire infrastructure deployment as a reusable script in a GitHub repository that any employer can review or run.
- Secret Mission: Isolated a backend subnet with a deny-all rule and subnet-scoped SSH access from the frontend subnet only.
Ready to quiz yourself?