Deploy a Secure Azure Virtual Network

Use Terraform to deploy a banking-style isolated network on Azure.

Introduction

30 Second Summary

Banks keep their most sensitive systems on completely separate networks. If an attacker breaches one part of the network, isolation stops them from reaching everything else. This is one of the most fundamental security patterns in cloud engineering.

In this project, you will deploy a secure Virtual Network on Azure using Terraform. You will write Infrastructure as Code that creates isolated subnets and firewall rules, exactly the way banks do it in production.

What You'll Build

A banking-style isolated network environment on Azure, created entirely through Infrastructure as Code using Terraform configuration files.

By the end of this project, you'll have:

  • A Terraform project with reusable configuration files that define your entire cloud infrastructure.
  • An Azure Virtual Network with isolated public and private subnets that separate different workloads.
  • Network Security Groups with banking-style firewall rules that restrict traffic between subnets.
  • Secret Mission: Add a third management subnet with a bastion-style access pattern where only the management subnet can reach the private subnet.

Are there any prerequisites?

You need a Microsoft account (for Azure signup), a credit or debit card for identity verification (you will not be charged), and VS Code installed on your machine. No prior Azure or Terraform experience is required.

Install Azure CLI and Terraform

Before you can deploy any cloud infrastructure, you need two tools installed on your machine. Azure CLI lets you communicate with Azure from your terminal. Terraform reads your configuration files and turns them into real cloud resources.

You also need an Azure account so Terraform has somewhere to deploy your network. In this step, you will install both tools and connect them to your Azure account.

In this step, get ready to:
  • Install Azure CLI and verify it works.
  • Install Terraform and add it to your system PATH.
  • Create a free Azure account and authenticate from your terminal.
Open your terminal

You will run all installation commands in PowerShell.

  • Press the Windows key to open your search bar.
  • Type PowerShell and press Enter to open it.
Install Azure CLI

Azure CLI is a command-line tool that lets you manage Azure resources directly from your terminal. Windows has a built-in package manager called WinGet that can install it in one command.

  • Install Azure CLI by running this command:
winget install --exact --id Microsoft.AzureCLI

What does this command do?

WinGet is the Windows package manager (similar to an app store for your terminal). The --exact --id flags tell WinGet to find the exact package named Microsoft.AzureCLI so there is no ambiguity about what gets installed.

Accept any prompts

WinGet may ask you to accept license agreements or source terms during installation. Type Y and press Enter if prompted.

  • Close your PowerShell window completely and reopen it (this ensures the new tool is recognized).
  • Verify Azure CLI is installed by running:
az version
  • You should see a JSON block showing the Azure CLI version (2.85.0 or higher).

Command not recognized?

If PowerShell says az is not recognized, make sure you fully closed and reopened your terminal after the install finished. WinGet installs the tool, but your existing terminal session does not know about it until restarted.

help me troubleshoot why az command is not recognized after installing Azure CLI with winget

Install Terraform

Terraform does not have an official Windows package manager install. You will download the binary manually and tell Windows where to find it.

  • Open your browser and go to the Terraform install page:
  • https://developer.hashicorp.com/terraform/install
  • Scroll down to the Windows section.
  • Click the AMD64 download link under Binary download. This downloads a zip file containing the Terraform executable.

Why AMD64?

AMD64 is the standard architecture for most Windows PCs and laptops. Unless you are on a Windows ARM device (like a Surface Pro X), AMD64 is the correct choice.

  • Open File Explorer and create a new folder at C:\terraform.
  • Open the downloaded zip file and extract terraform.exe into C:\terraform.

Windows needs to know where to find terraform.exe when you type terraform in your terminal. You do this by adding the folder to your system PATH.

  • Press the Windows key and type environment variables.
  • Click Edit the system environment variables.
  • In the System Properties window, click Environment Variables.
  • Under System variables, find and select the row named Path, then click Edit.
  • Click New and type C:\terraform.
  • Click OK on all three open windows to save your changes.

What is the system PATH?

The PATH is a list of folders Windows searches when you type a command. By adding C:\terraform to this list, Windows knows to look there when you run terraform.

  • Close your PowerShell window completely and reopen it.
  • Verify Terraform is installed by running:
terraform version
  • You should see output showing Terraform v1.15.1 (or a similar version starting with 1.).

Terraform is not recognized?

If you see terraform: The term 'terraform' is not recognized, double check these things:

  • Make sure terraform.exe is directly inside C:\terraform (not in a subfolder like C:\terraform\terraform_1.15.1_windows_amd64\).
  • Confirm your PATH entry is exactly C:\terraform with no trailing slash.
  • Make sure you closed and reopened PowerShell after editing the PATH.

help me troubleshoot why terraform command is not recognized after I downloaded the binary and added it to my PATH on Windows

Create your free Azure account and sign in

Terraform needs an Azure account to deploy resources into. Azure offers a free account with $200 credit for 30 days, which is more than enough for this project.

  • Open your browser and go to the Azure free account page:
  • https://azure.microsoft.com/free
  • Click Start free.
  • Sign in with your Microsoft account.
  • Complete the signup form with your phone number and credit/debit card.

Will I be charged?

No. Azure requires a credit or debit card (non-prepaid) for identity verification only. Your card is not charged. You may see a temporary $1 hold that gets reversed automatically.

Once your account is created, you need to connect your terminal to it.

  • Back in PowerShell, authenticate to Azure by running:
az login

What does this command do?

This opens your default browser and asks you to sign in with your Microsoft account. Once you sign in, the browser sends your credentials back to the terminal so Azure CLI can make requests on your behalf.

  • A browser window opens automatically. Sign in with the same Microsoft account you used to create your Azure free account.
  • After signing in, return to your terminal. You should see a JSON response listing your subscription details (including Azure subscription 1 or similar).

No subscriptions found?

If the output says no subscriptions were found, your free account signup may not have completed. Go back to https://azure.microsoft.com/free and make sure you finished all the steps including the card verification.

help me fix az login showing no subscriptions after I created my Azure free account

Your tools are installed and connected. Next up, you will create your Terraform project files and write your first infrastructure configuration.

Create Your Terraform Project Structure

Your tools are installed and your Azure account is ready. Now it's time to create the Terraform project that will define your banking network infrastructure.

Terraform projects use a specific file structure to organize infrastructure code. Each file has a dedicated purpose, which keeps your configuration clean and reusable as it grows.

In this step, get ready to:
  • Create a Terraform project folder with three configuration files.
  • Write the provider configuration that connects Terraform to Azure.
  • Run terraform init to download the Azure provider plugin.
Create your project folder and files

Terraform needs a dedicated folder to store its configuration. You'll create three files that each serve a specific role in your project.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir banking-terraform
cd banking-terraform
  • Create the three Terraform configuration files by running:
New-Item main.tf, variables.tf, outputs.tf
  • Confirm the files exist by running:
ls

You should see main.tf, variables.tf, and outputs.tf in the output.

What are these three files for?

  • main.tf holds your infrastructure resources (networks, security groups, and other cloud components).
  • variables.tf defines configurable values like region and resource names, so you can reuse your code without editing it directly.
  • outputs.tf displays useful information (like resource IDs) after Terraform finishes deploying.
  • Open this folder in VS Code by running:
code .

VS Code opens with your banking-terraform folder in the Explorer sidebar. You should see your three empty files listed.

VS Code didn't open?

If you see code : The term 'code' is not recognized, the code command is not in your PATH. Open VS Code manually instead.

  • Press the Windows key, type VS Code, and press Enter.
  • Click File, then Open Folder, and navigate to Desktop > banking-terraform.

help me fix the code command in my terminal

Write the provider configuration and variables

Terraform needs to know which cloud platform you're targeting. A provider configuration tells Terraform to use the Azure plugin and authenticate with your CLI credentials.

  • In VS Code, click on main.tf in the Explorer sidebar to open it.
  • Paste the following provider configuration into main.tf:
terraform {
  required_version = ">= 1.5.0"

  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.0"
    }
  }
}

provider "azurerm" {
  features {}
}

What does this code do?

  • required_version = ">= 1.5.0" ensures anyone running this project has a compatible Terraform version.
  • source = "hashicorp/azurerm" tells Terraform to download the official Azure provider plugin from the Terraform Registry.
  • version = "~> 4.0" pins the provider to version 4.x, preventing unexpected breaking changes from a future major release.
  • features {} is required by the AzureRM provider even when empty. It enables optional provider behaviours.
  • Save main.tf by pressing Ctrl+S.

Now you'll define the configurable values that your resources will reference. This keeps your project flexible.

  • Click on variables.tf in the Explorer sidebar to open it.
  • Paste the following variable definitions into variables.tf:
variable "resource_group_name" {
  description = "Name of the Azure Resource Group"
  type        = string
  default     = "banking-network-rg"
}

variable "location" {
  description = "Azure region for all resources"
  type        = string
  default     = "East US"
}

What does this code do?

Variables let you change values like region or resource names without editing your main configuration files.

  • resource_group_name sets the name of the container that groups all your Azure resources together.
  • location determines which Azure data centre region your resources deploy to. "East US" is a common default with broad service availability.
  • Save variables.tf by pressing Ctrl+S.

Seeing red squiggly lines in VS Code?

VS Code may not recognise .tf files by default. This is purely cosmetic and does not affect Terraform. If you want syntax highlighting, install the HashiCorp Terraform extension from the VS Code Extensions panel.

help me set up the Terraform extension in VS Code

Initialize your Terraform project

Before Terraform can deploy anything, it needs to download the provider plugin you specified. The terraform init command handles this. It is always the first command you run in a new Terraform project.

  • Open the integrated terminal in VS Code by pressing Ctrl+` (the backtick key, usually above Tab).
  • Initialize your Terraform project by running:
terraform init

Terraform downloads the AzureRM provider plugin and creates a .terraform directory in your project folder. You should see a success message that says Terraform has been successfully initialized!

What did terraform init create?

  • .terraform/ is a hidden folder that stores the downloaded provider plugin binary. You never edit this directly.
  • .terraform.lock.hcl locks the exact provider version that was downloaded. This ensures everyone on your team uses the same version.

Seeing an error during init?

  • If you see Error: Failed to query available provider packages, check your internet connection. Terraform needs to download the provider from the registry.
  • If you see Error: Inconsistent dependency lock file, delete the .terraform folder and .terraform.lock.hcl file, then run terraform init again.
  • Make sure your terminal is in the banking-terraform folder. Run pwd to check your current location.

help me troubleshoot terraform init errors

✔️ Awesome, I've got everything!

Great. Double check you've saved both main.tf and variables.tf before moving on.

ⓧ I'd like to double check the full code

Here is the complete code for each file at the end of this step.

terraform {
  required_version = ">= 1.5.0"

  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.0"
    }
  }
}

provider "azurerm" {
  features {}
}
variable "resource_group_name" {
  description = "Name of the Azure Resource Group"
  type        = string
  default     = "banking-network-rg"
}

variable "location" {
  description = "Azure region for all resources"
  type        = string
  default     = "East US"
}

outputs.tf remains empty for now. You'll add output definitions in a later step.

Your Terraform project is initialized and connected to Azure. Next up, you'll write the actual infrastructure resources and deploy them to the cloud.

Deploy a Resource Group and Virtual Network

Your Terraform project is initialized and connected to the Azure provider. Now it's time to define the actual cloud resources that form your banking network.

Every cloud deployment starts with a Resource Group, which is a container that holds related resources together. Inside it, you'll create a Virtual Network with two subnets to separate public and private workloads.

In this step, get ready to:
  • Add a Resource Group and Virtual Network to your Terraform configuration.
  • Create public and private subnets to isolate network traffic.
  • Deploy all resources to Azure with terraform plan and terraform apply.
Add the Resource Group and Virtual Network

A Resource Group is a logical container that groups related Azure resources. A Virtual Network defines the IP address range for your isolated network.

  • In main.tf, add the Resource Group and Virtual Network resources below the existing provider "azurerm" block by copying this code:
# Container for all banking network resources
resource "azurerm_resource_group" "banking_rg" {
  name     = var.resource_group_name
  location = var.location
}

# The main Virtual Network with a /16 address space (65,536 IP addresses)
resource "azurerm_virtual_network" "banking_vnet" {
  name                = "banking-vnet"
  location            = azurerm_resource_group.banking_rg.location
  resource_group_name = azurerm_resource_group.banking_rg.name
  address_space       = ["10.0.0.0/16"]

  tags = {
    environment = "learning"
    project     = "banking-network"
  }
}

What does this code do?

  • azurerm_resource_group creates a container in Azure called banking-network-rg in the East US region. The name and location come from the variables you defined earlier.
  • azurerm_virtual_network creates a network with the address space 10.0.0.0/16, giving you 65,536 possible IP addresses to work with.
  • The VNet references the resource group using azurerm_resource_group.banking_rg.name. This creates a dependency so Terraform knows to create the group first.
  • Save main.tf.
  • You won't see a visible change yet since you haven't deployed. The next chunk adds subnets, then you'll deploy everything together.

Seeing red squiggly lines in VS Code?

Make sure you have the HashiCorp Terraform extension installed in VS Code. It provides syntax highlighting and error checking for .tf files.

Also check that the code is placed after the closing } of the provider "azurerm" block, not inside it.

help me fix syntax errors in my main.tf file

Add the public and private subnets

Banks separate their networks into zones. A public subnet handles internet-facing traffic. A private subnet holds sensitive backend systems that should never be directly accessible from the internet.

  • Add the two subnet resources to main.tf below the Virtual Network block:
# Public-facing subnet for internet-accessible resources
resource "azurerm_subnet" "public" {
  name                 = "public-subnet"
  resource_group_name  = azurerm_resource_group.banking_rg.name
  virtual_network_name = azurerm_virtual_network.banking_vnet.name
  address_prefixes     = ["10.0.1.0/24"]
}

# Private subnet for backend systems (no direct internet access)
resource "azurerm_subnet" "private" {
  name                 = "private-subnet"
  resource_group_name  = azurerm_resource_group.banking_rg.name
  virtual_network_name = azurerm_virtual_network.banking_vnet.name
  address_prefixes     = ["10.0.2.0/24"]
}

What does this code do?

  • Each subnet carves out a /24 slice (256 IP addresses) from the larger /16 Virtual Network.
  • public-subnet uses the 10.0.1.0/24 range for resources that need internet access.
  • private-subnet uses 10.0.2.0/24 for backend systems that stay hidden from the internet.
  • Both subnets reference the VNet by name, so Terraform creates the VNet before attempting to create the subnets.
  • Save main.tf.
  • You won't see a change yet. The next substep deploys everything to Azure.

Seeing an error about the subnet resource?

Make sure virtual_network_name references azurerm_virtual_network.banking_vnet.name exactly. A typo here breaks the dependency chain.

Verify each subnet block is placed outside (after) the VNet block's closing }, not nested inside it.

help me fix my subnet resource definition

✔️ Awesome, I've got everything!

Great. Double check you've saved main.tf before moving on.

ⓧ I'd like to double check the full code

terraform {
  required_version = ">= 1.5.0"

  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.0"
    }
  }
}

provider "azurerm" {
  features {}
}

resource "azurerm_resource_group" "banking_rg" {
  name     = var.resource_group_name
  location = var.location
}

resource "azurerm_virtual_network" "banking_vnet" {
  name                = "banking-vnet"
  location            = azurerm_resource_group.banking_rg.location
  resource_group_name = azurerm_resource_group.banking_rg.name
  address_space       = ["10.0.0.0/16"]

  tags = {
    environment = "learning"
    project     = "banking-network"
  }
}

resource "azurerm_subnet" "public" {
  name                 = "public-subnet"
  resource_group_name  = azurerm_resource_group.banking_rg.name
  virtual_network_name = azurerm_virtual_network.banking_vnet.name
  address_prefixes     = ["10.0.1.0/24"]
}

resource "azurerm_subnet" "private" {
  name                 = "private-subnet"
  resource_group_name  = azurerm_resource_group.banking_rg.name
  virtual_network_name = azurerm_virtual_network.banking_vnet.name
  address_prefixes     = ["10.0.2.0/24"]
}
Deploy your infrastructure to Azure

Before deploying, you'll add output definitions that tell Terraform to display key information after each apply. Then you'll preview and deploy your infrastructure.

  • First, confirm you're still signed in to Azure by running this command:
az account show

✔️ I see my account details

You're signed in and ready to deploy. Continue below.

ⓧ I'm not signed in

  • Sign in to Azure by running:
az login

This opens a browser window. Sign in with the Microsoft account you used to create your Azure free account.

  • Add the following output definitions to outputs.tf so Terraform displays useful information after deployment:
# Display the resource group name after deployment
output "resource_group_name" {
  description = "Name of the created resource group"
  value       = azurerm_resource_group.banking_rg.name
}

# Display the VNet name
output "vnet_name" {
  description = "Name of the virtual network"
  value       = azurerm_virtual_network.banking_vnet.name
}

# Display the full address space
output "vnet_address_space" {
  description = "Address space of the virtual network"
  value       = azurerm_virtual_network.banking_vnet.address_space
}

# Display the unique ID of the public subnet
output "public_subnet_id" {
  description = "ID of the public subnet"
  value       = azurerm_subnet.public.id
}

# Display the unique ID of the private subnet
output "private_subnet_id" {
  description = "ID of the private subnet"
  value       = azurerm_subnet.private.id
}

What do outputs do?

Outputs tell Terraform to print specific values after a successful terraform apply. Each output has a description for documentation and a value that references an attribute of a resource you defined.

These IDs and names become useful when connecting other resources (like Network Security Groups) in the next step.

  • Save outputs.tf.

✔️ Awesome, I've got everything!

Great. Double check you've saved outputs.tf before moving on.

ⓧ I'd like to double check the full code

output "resource_group_name" {
  description = "Name of the created resource group"
  value       = azurerm_resource_group.banking_rg.name
}

output "vnet_name" {
  description = "Name of the virtual network"
  value       = azurerm_virtual_network.banking_vnet.name
}

output "vnet_address_space" {
  description = "Address space of the virtual network"
  value       = azurerm_virtual_network.banking_vnet.address_space
}

output "public_subnet_id" {
  description = "ID of the public subnet"
  value       = azurerm_subnet.public.id
}

output "private_subnet_id" {
  description = "ID of the private subnet"
  value       = azurerm_subnet.private.id
}
  • Preview what Terraform will create by running this command:
terraform plan

You should see output showing Plan: 4 to add, 0 to change, 0 to destroy. The four resources are your resource group, virtual network, and two subnets.

What is terraform plan?

terraform plan is a dry run. It compares your configuration files against the current state (nothing deployed yet) and shows exactly what it will create, modify, or destroy. Always review the plan before applying.

  • Deploy your resources to Azure by running:
terraform apply
  • Terraform shows you the plan again and asks for confirmation. Type yes and press Enter.

After 30-60 seconds, you should see Apply complete! Resources: 4 added, 0 changed, 0 destroyed. along with your output values printed below.

Seeing an error during apply?

  • If you see Error: building AzureRM Client, your Azure session has expired. Run az login to sign in again, then re-run terraform apply.
  • If you see Error: creating Resource Group, check that your Azure free trial is active by visiting portal.azure.com.
  • If you see Error: Insufficient permissions, make sure you're signed in with the same Microsoft account that owns the Azure subscription.

help me fix my terraform apply error

Your Virtual Network is live in Azure with two isolated subnets. Next up, you'll add Network Security Groups with firewall rules that control exactly which traffic can flow between them.

Add Network Security Groups with Banking-Style Rules

Your Virtual Network has two subnets, but right now any traffic can flow anywhere. In a real banking environment, that is a compliance violation.

Banks enforce strict traffic rules between network segments using firewalls. In Azure, the equivalent is a Network Security Group (NSG). Each NSG contains ordered rules that allow or deny traffic based on source, destination, port, and protocol. In this step, you will create two NSGs that lock down your subnets the way a bank would.

In this step, get ready to:
  • Create a public NSG that allows SSH but blocks everything else from the internet.
  • Create a private NSG that only accepts traffic from the public subnet.
  • Associate each NSG with its subnet and deploy the security rules.
Create the public subnet NSG

The public subnet is where internet-facing resources live. You want to allow administrators to connect via SSH (port 22) but deny all other inbound internet traffic.

  • In VS Code, open main.tf.
  • Add the following code at the bottom of the file, below the azurerm_subnet.private resource:
resource "azurerm_network_security_group" "public_nsg" {
  name                = "public-nsg"
  location            = azurerm_resource_group.banking_rg.location
  resource_group_name = azurerm_resource_group.banking_rg.name

  # Rule 1: Allow SSH connections from anywhere
  security_rule {
    name                       = "AllowSSH"
    priority                   = 100
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "22"
    source_address_prefix      = "*"
    destination_address_prefix = "*"
  }

  # Rule 2: Deny all other inbound traffic from the internet
  security_rule {
    name                       = "DenyAllInbound"
    priority                   = 200
    direction                  = "Inbound"
    access                     = "Deny"
    protocol                   = "*"
    source_port_range          = "*"
    destination_port_range     = "*"
    source_address_prefix      = "Internet"
    destination_address_prefix = "*"
  }

  tags = {
    environment = "learning"
  }
}

What does this code do?

This creates a Network Security Group called public-nsg with two rules that are evaluated in priority order (lowest number first).

  • Priority 100 (AllowSSH): Allows inbound TCP traffic on port 22 from any source. This is how administrators connect to servers remotely.
  • Priority 200 (DenyAllInbound): Blocks all other inbound traffic from the internet. The Internet source tag is an Azure service tag that represents all public IP addresses.

What is a Network Security Group?

An NSG is Azure's built-in firewall for subnets and network interfaces. It contains a list of security rules that filter traffic based on 5-tuple matching: source, destination, port, protocol, and direction.

Rules are processed in priority order. Once a rule matches, Azure stops checking further rules. Lower priority numbers are evaluated first.

  • Save main.tf.

Seeing red underlines in VS Code?

If VS Code shows syntax errors, check that your curly braces are balanced. Each security_rule { } block must be nested inside the azurerm_network_security_group resource block.

help me fix syntax errors in my Terraform NSG configuration

Create the private subnet NSG

The private subnet holds backend systems that should never be reached directly from the internet. The only legitimate traffic comes from the public subnet.

  • Add the following code below the public_nsg resource you just created in main.tf:
resource "azurerm_network_security_group" "private_nsg" {
  name                = "private-nsg"
  location            = azurerm_resource_group.banking_rg.location
  resource_group_name = azurerm_resource_group.banking_rg.name

  # Rule 1: Allow all traffic from the public subnet
  security_rule {
    name                       = "AllowFromPublicSubnet"
    priority                   = 100
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "*"
    source_port_range          = "*"
    destination_port_range     = "*"
    source_address_prefix      = "10.0.1.0/24"
    destination_address_prefix = "*"
  }

  # Rule 2: Deny all inbound traffic from the internet
  security_rule {
    name                       = "DenyInternetInbound"
    priority                   = 200
    direction                  = "Inbound"
    access                     = "Deny"
    protocol                   = "*"
    source_port_range          = "*"
    destination_port_range     = "*"
    source_address_prefix      = "Internet"
    destination_address_prefix = "*"
  }

  tags = {
    environment = "learning"
  }
}

What does this code do?

This NSG protects the private subnet with two rules:

  • Priority 100 (AllowFromPublicSubnet): Allows all traffic from 10.0.1.0/24 (the public subnet CIDR). This means only resources in your public subnet can talk to the private subnet.
  • Priority 200 (DenyInternetInbound): Blocks all direct internet traffic. No one on the internet can reach your private subnet.

This is the exact isolation pattern banks use to protect databases and internal APIs from direct internet exposure.

  • Save main.tf.

Not sure where to add this code?

Place it directly after the closing brace of the public_nsg resource. Each resource block in Terraform is independent, so order does not technically matter, but keeping related resources together makes your file easier to read.

help me organize my Terraform resources

Associate NSGs with subnets and deploy

Creating an NSG does not automatically attach it to anything. You need an association resource that links each NSG to its target subnet.

  • Add the following code below the private_nsg resource in main.tf:
# Link NSGs to their respective subnets
resource "azurerm_subnet_network_security_group_association" "public_nsg_assoc" {
  subnet_id                 = azurerm_subnet.public.id
  network_security_group_id = azurerm_network_security_group.public_nsg.id
}

resource "azurerm_subnet_network_security_group_association" "private_nsg_assoc" {
  subnet_id                 = azurerm_subnet.private.id
  network_security_group_id = azurerm_network_security_group.private_nsg.id
}

What does this code do?

Each association resource takes a subnet_id and a network_security_group_id. Terraform uses references like azurerm_subnet.public.id to automatically resolve the correct IDs from your existing resources.

Without these associations, your NSGs would exist in Azure but would not filter any traffic.

Now add two outputs so you can quickly reference your NSG names later.

  • Open outputs.tf and add the following at the bottom of the file:
output "public_nsg_name" {
  description = "Name of the public network security group"
  value       = azurerm_network_security_group.public_nsg.name
}

output "private_nsg_name" {
  description = "Name of the private network security group"
  value       = azurerm_network_security_group.private_nsg.name
}

What does this code do?

These outputs print the NSG names to your terminal after each terraform apply. They also make these values accessible to other Terraform configurations if you reference this project as a module in the future.

  • Save both main.tf and outputs.tf.
  • Deploy your security rules by running this command in your terminal:
terraform apply
  • Terraform will show a plan with 4 new resources to add (2 NSGs and 2 associations). Type yes and press Enter to confirm.

You should see output ending with Apply complete! Resources: 4 added, 0 changed, 0 destroyed. along with your new output values for public_nsg_name and private_nsg_name.

Seeing an error about duplicate resources?

Make sure each resource block has a unique name. You should have "public_nsg" and "private_nsg" as separate identifiers. Also confirm you did not accidentally paste a resource inside another resource block.

help me fix my terraform apply error

✔️ Awesome, I've got everything!

Great. Double check you have saved both files before moving on.

ⓧ I'd like to double check the full code

Here is the complete main.tf at this point:

terraform {
  required_version = ">= 1.5.0"

  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.0"
    }
  }
}

provider "azurerm" {
  features {}
}

resource "azurerm_resource_group" "banking_rg" {
  name     = var.resource_group_name
  location = var.location
}

resource "azurerm_virtual_network" "banking_vnet" {
  name                = "banking-vnet"
  location            = azurerm_resource_group.banking_rg.location
  resource_group_name = azurerm_resource_group.banking_rg.name
  address_space       = ["10.0.0.0/16"]

  tags = {
    environment = "learning"
    project     = "banking-network"
  }
}

resource "azurerm_subnet" "public" {
  name                 = "public-subnet"
  resource_group_name  = azurerm_resource_group.banking_rg.name
  virtual_network_name = azurerm_virtual_network.banking_vnet.name
  address_prefixes     = ["10.0.1.0/24"]
}

resource "azurerm_subnet" "private" {
  name                 = "private-subnet"
  resource_group_name  = azurerm_resource_group.banking_rg.name
  virtual_network_name = azurerm_virtual_network.banking_vnet.name
  address_prefixes     = ["10.0.2.0/24"]
}

resource "azurerm_network_security_group" "public_nsg" {
  name                = "public-nsg"
  location            = azurerm_resource_group.banking_rg.location
  resource_group_name = azurerm_resource_group.banking_rg.name

  security_rule {
    name                       = "AllowSSH"
    priority                   = 100
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "22"
    source_address_prefix      = "*"
    destination_address_prefix = "*"
  }

  security_rule {
    name                       = "DenyAllInbound"
    priority                   = 200
    direction                  = "Inbound"
    access                     = "Deny"
    protocol                   = "*"
    source_port_range          = "*"
    destination_port_range     = "*"
    source_address_prefix      = "Internet"
    destination_address_prefix = "*"
  }

  tags = {
    environment = "learning"
  }
}

resource "azurerm_network_security_group" "private_nsg" {
  name                = "private-nsg"
  location            = azurerm_resource_group.banking_rg.location
  resource_group_name = azurerm_resource_group.banking_rg.name

  security_rule {
    name                       = "AllowFromPublicSubnet"
    priority                   = 100
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "*"
    source_port_range          = "*"
    destination_port_range     = "*"
    source_address_prefix      = "10.0.1.0/24"
    destination_address_prefix = "*"
  }

  security_rule {
    name                       = "DenyInternetInbound"
    priority                   = 200
    direction                  = "Inbound"
    access                     = "Deny"
    protocol                   = "*"
    source_port_range          = "*"
    destination_port_range     = "*"
    source_address_prefix      = "Internet"
    destination_address_prefix = "*"
  }

  tags = {
    environment = "learning"
  }
}

resource "azurerm_subnet_network_security_group_association" "public_nsg_assoc" {
  subnet_id                 = azurerm_subnet.public.id
  network_security_group_id = azurerm_network_security_group.public_nsg.id
}

resource "azurerm_subnet_network_security_group_association" "private_nsg_assoc" {
  subnet_id                 = azurerm_subnet.private.id
  network_security_group_id = azurerm_network_security_group.private_nsg.id
}

And here is the complete outputs.tf:

output "resource_group_name" {
  description = "Name of the created resource group"
  value       = azurerm_resource_group.banking_rg.name
}

output "vnet_name" {
  description = "Name of the virtual network"
  value       = azurerm_virtual_network.banking_vnet.name
}

output "vnet_address_space" {
  description = "Address space of the virtual network"
  value       = azurerm_virtual_network.banking_vnet.address_space
}

output "public_subnet_id" {
  description = "ID of the public subnet"
  value       = azurerm_subnet.public.id
}

output "private_subnet_id" {
  description = "ID of the private subnet"
  value       = azurerm_subnet.private.id
}

output "public_nsg_name" {
  description = "Name of the public network security group"
  value       = azurerm_network_security_group.public_nsg.name
}

output "private_nsg_name" {
  description = "Name of the private network security group"
  value       = azurerm_network_security_group.private_nsg.name
}

Your network is now secured with banking-style firewall rules. Next up, you will verify the deployment from Azure's side and inspect everything Terraform is managing.

Verify Your Deployment

Your infrastructure is deployed, but how do you know it actually worked? In cloud engineering, verification is just as important as deployment.

In this step, you will query Azure directly using the Azure CLI and review Terraform's state file to confirm every resource exists exactly as you defined it.

In this step, get ready to:
  • Confirm the Virtual Network exists using the Azure CLI.
  • Verify both Network Security Groups are applied to their subnets.
  • Review all managed resources in Terraform's state.
Confirm the Virtual Network exists in Azure

The Azure CLI lets you query your cloud resources directly, independent of Terraform. This gives you a second source of truth to confirm your deployment succeeded.

  • Verify your Virtual Network exists by running this command:
az network vnet show --resource-group banking-network-rg --name banking-vnet --output table

You should see a table showing your banking-vnet with the address prefix 10.0.0.0/16 and its location.

Why verify outside of Terraform?

Terraform tracks what it thinks exists in its state file. The Azure CLI queries what actually exists in the cloud. Comparing both confirms there is no drift between your configuration and reality.

Seeing 'ResourceNotFound'?

Make sure you ran terraform apply successfully in the previous step. Check that --resource-group matches banking-network-rg exactly.

Help me troubleshoot a ResourceNotFound error when querying my Azure VNet.

Verify the Network Security Groups

Your NSGs contain the firewall rules that enforce traffic segmentation. Confirming they exist and are applied to the correct subnets is the most important verification in this project.

  • List all Network Security Groups in your resource group by running this command:
az network nsg list --resource-group banking-network-rg --output table

You should see two rows in the table: public-nsg and private-nsg.

What should I see?

The table output shows each NSG's name, location, resource group, and provisioning state. Both should show Succeeded as their provisioning state, confirming Azure created them without errors.

Only seeing one NSG?

Check that your main.tf contains both the public_nsg and private_nsg resource blocks. Run terraform apply again to deploy any missing resources.

Help me figure out why only one NSG was created.

Review all resources in Terraform state

Terraform keeps track of every resource it manages in a state file. Listing the state gives you a complete inventory of your infrastructure.

  • View the full list of resources Terraform is managing by running this command:
terraform state list

You should see eight resources listed:

  • azurerm_resource_group.banking_rg (the container for everything).
  • azurerm_virtual_network.banking_vnet (the network itself).
  • azurerm_subnet.public and azurerm_subnet.private (the two isolated segments).
  • azurerm_network_security_group.public_nsg and azurerm_network_security_group.private_nsg (the firewall rules).
  • azurerm_subnet_network_security_group_association.public_nsg_assoc and azurerm_subnet_network_security_group_association.private_nsg_assoc (linking NSGs to subnets).

What is Terraform state?

The state file is Terraform's record of what it has deployed. Every time you run terraform apply, Terraform compares your configuration files against this state to determine what needs to change.

This is why terraform destroy knows exactly what to delete. It reads the state file for the full list of resources it created.

  • View the full details of your deployed infrastructure by running:
terraform show

You should see a detailed output showing every resource with its properties: IP ranges, security rule priorities, subnet associations, and tags.

What does terraform show tell me?

While terraform state list gives you resource names, terraform show gives you every attribute of every resource. This is useful for debugging or confirming exact values like CIDR ranges and rule priorities.

Seeing 'No state file found'?

Make sure you are running the command from inside your project folder (the one containing main.tf). Terraform looks for the terraform.tfstate file in the current directory.

Help me fix a missing state file error.

Secret mission

Add a Management Subnet with Bastion-Style Access

In production banking environments, nobody SSHs directly into servers from the internet. All access goes through a dedicated management subnet first. Can you add a third subnet that acts as the only entry point to your network?

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. These networking resources cost fractions of a cent per day against your $200 Azure free credit, so there is no urgency to delete them.

Cost warning

Your Azure free account comes with $200 of credit for 30 days. The Virtual Network and Network Security Group resources created in this project cost almost nothing. However, if you leave them running after your free trial expires, you may see small charges on your card.

If your free trial is close to expiring and you are done with this project, choose the Delete option below to remove all resources.

Resources you used:

  • 1 Azure Resource Group (banking-network-rg).
  • 1 Virtual Network with 2 subnets (or 3 if you completed the Secret Mission).
  • 2 Network Security Groups (or 3 if you completed the Secret Mission).
  • Local Terraform project folder on your Desktop.
  • Azure CLI and Terraform installed on your machine.

Keep everything running

No action needed. Choose this if you are still actively building or want to practice more Terraform commands against your live infrastructure.

  • Your resources remain deployed in Azure and continue using negligible credit.
  • You can continue running terraform plan and terraform apply to experiment with changes.
  • Your local project files stay on your Desktop for future edits.

Pause - I'll come back to this later

Networking resources have minimal ongoing cost, so there is nothing to pause or shut down. Your resources will sit idle at virtually no cost until you return.

  • No action needed for the Azure resources. Virtual Networks and NSGs do not consume meaningful credit when idle.
  • Keep your local project folder intact so you can run terraform apply again when you return.
  • When you come back, run az login to re-authenticate before running any Terraform commands.

Delete - I don't want to use this again

Remove all Azure resources and start fresh. This stops any charges completely.

  • Open your terminal and navigate to your Terraform project folder.
  • Run the following command to destroy all resources:
terraform destroy
  • Type yes when prompted to confirm the destruction.
  • Wait for Terraform to remove all resources. You should see a message confirming all resources have been destroyed.
  • Optionally, delete your local project folder from your Desktop if you no longer need the files.
  • Azure CLI and Terraform can remain installed on your machine for future projects.

That's a Wrap!

That's a Wrap!

Nice work! You just deployed a banking-style secure network on Azure using Infrastructure as Code.

You've learned how to:

  • Written Terraform HCL configuration files to define cloud infrastructure as reusable code.
  • Deployed a Virtual Network with isolated public and private subnets on Azure.
  • Created Network Security Groups with firewall rules that enforce traffic segmentation between subnets.
  • Secret Mission: Added a management subnet with bastion-style access control to prevent direct internet exposure of private systems.

Ready to quiz yourself?