Episode #2: Building a Dynamic Virtual Network with Terraform’s 'azurerm' Provider
Welcome to the second episode of Azure Terraformer, where we dive deep into using Terraform on Azure to set up powerful, scalable cloud solutions. Today, we’ll be setting up a software-defined network with the Azure Terraform provider. Which can be surprisingly straightforward, offering a balance of flexibility and control. In this guide, I’ll walk through creating a virtual network (VNet) in Azure, complete with subnets, network security groups (NSGs), and rules—all using Terraform’s Azurerm provider. Here, I’m taking a slightly unconventional approach by keeping resource blocks independent, rather than nesting them. This modular style is intentional; it keeps the network configuration adaptable, making it easier to adjust and expand as requirements evolve. To avoid potential naming conflicts, we’ll also use Terraform’s random_string
resource to generate dynamic names for each resource. Let’s dive in.
Step 1: Creating the Resource Group
Every Azure resource needs a home, and the resource group is the fundamental container that organizes them. Here, we start by creating the resource group with a unique name generated using random_string
. By using a dynamic name, we avoid potential naming conflicts with existing resources, keeping the infrastructure clean and manageable.
resource "random_string" "main" {
length = 8
upper = false
special = false
}
resource "azurerm_resource_group" "main" {
name = "rg-ep2-${random_string.main.result}"
location = var.location
}
In this configuration, the random_string
resource produces an eight-character string in lowercase, which is appended to the resource group’s base name rg-ep2-
. This keeps the naming both unique and predictable.
Step 2: Defining the Virtual Network (VNet)
With the resource group in place, it’s time to define the virtual network. I’m opting for a /16
CIDR block here purely for simplicity, as it provides ample IP space. The VNet inherits its location from the resource group, maintaining consistency.
resource "azurerm_virtual_network" "main" {
name = "vnet-ep2-${random_string.main.result}"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
address_space = ["10.0.0.0/16"]
}
Naming the VNet with a unique suffix (using the same random string as the resource group) prevents potential conflicts and helps easily identify related resources. This standalone VNet resource lets us treat it independently, allowing future modification without impacting any nested configurations.
Step 3: Creating the Subnet
For this example, I’m defining subnets as separate resources rather than embedding them within the VNet. This approach gives each subnet its own Terraform block, making it simpler to adjust subnet configurations without needing to modify the VNet directly.
resource "azurerm_subnet" "default" {
name = "snet-default"
resource_group_name = azurerm_resource_group.main.name
virtual_network_name = azurerm_virtual_network.main.name
address_prefixes = ["10.0.1.0/24"]
}
This subnet, snet-default
, occupies the 10.0.1.0/24
range within our VNet. Keeping the subnet configuration isolated makes it easy to add additional subnets or alter this one without disrupting the broader VNet resource.
Step 4: Configuring the Network Security Group (NSG)
A Network Security Group controls inbound and outbound traffic rules for Azure resources. Here, we’re creating a default NSG as an independent resource within the resource group, laying the groundwork for fine-tuned security control over our subnet.
resource "azurerm_network_security_group" "default" {
name = "nsg-default"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
}
Again, keeping the NSG in its own resource block, rather than nesting it within the subnet, enables more targeted updates to security configurations without interference from subnet-specific changes.
Step 5: Adding an NSG Rule
To control network access, we’ll define an NSG rule that allows SSH access on port 22 from my current public IP address. To automate the process of retrieving my IP, I’m using a data source to pull it dynamically. This rule is an independent resource, giving us flexibility to add or modify rules as needed.
data "http" "myip" {
url = "http://ipv4.icanhazip.com"
}
resource "azurerm_network_security_rule" "rule1" {
resource_group_name = azurerm_resource_group.main.name
network_security_group_name = azurerm_network_security_group.default.name
name = "rule-100"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "22"
source_address_prefix = "${chomp(data.http.myip.body)}/32"
destination_address_prefix = "*"
}
Using the data "http"
resource to pull the public IP simplifies managing access. We’re specifying this rule independently of the NSG, preserving modularity. The rule is easy to modify, remove, or duplicate as needed, providing granular control over traffic policies.
Step 6: Associating the NSG with the Subnet
With the NSG and its rule in place, we can associate the NSG with the subnet. This final step applies the security controls to the desired subnet while maintaining modularity. The association is defined in a standalone resource, allowing for easy reassignment to other subnets if needed.
resource "azurerm_subnet_network_security_group_association" "default_rule1" {
subnet_id = azurerm_subnet.default.id
network_security_group_id = azurerm_network_security_group.default.id
}
This association block links our NSG to the snet-default
subnet. Since the association is its own resource, it remains flexible—if we add additional subnets or modify NSG rules, we can adapt without revisiting the main subnet configuration.
Conclusion
By structuring the network setup in this modular way, we’re building a configuration that’s both flexible and easy to expand. Each component—the resource group, VNet, subnet, NSG, and rules—exists as an independent Terraform resource, meaning updates can be made to one piece without triggering unexpected changes across the rest of the configuration. This approach not only avoids naming conflicts with random_string
but also sets up a scalable network foundation that can evolve alongside new requirements. In a world where network configurations constantly need to adapt to new security and connectivity demands, this extensibility is invaluable.
Happy Azure Terraforming!