Architecting Azure Networking with Terraform: A Deep Dive into azurerm_virtual_network

Azure Virtual Networks (VNets) serve as the fundamental infrastructure for private networking within the Microsoft Azure cloud platform. They function as the logical foundation upon which all private communication between Azure resources is built. In the modern DevOps landscape, managing this critical infrastructure through Infrastructure as Code (IaC) is no longer optional but a mandatory best practice. Terraform, developed by HashiCorp, has become the de facto standard for defining, versioning, and replicating complex network topologies. By utilizing the azurerm_virtual_network resource, engineers can ensure that their network architecture is consistent, auditable, and resilient across different environments. Getting the network architecture right at the start is critical because changing it later can require readdressing or migrating workloads, a process that is often costly and disruptive. Terraform simplifies this by allowing the network topology to be defined in code, previewed via execution plans, and deployed with high precision.

This article provides a comprehensive technical analysis of implementing Azure VNets using Terraform. It covers provider configuration, basic resource provisioning, advanced subnet management including service endpoints and delegations, dynamic subnet creation, and the deployment of Azure Virtual Network Manager for complex mesh topologies.

Prerequisites and Environment Setup

Before deploying any infrastructure, the local environment must be prepared to interface with the Azure provider. The following prerequisites are required to ensure a smooth deployment workflow:

  • Terraform version 1.3 or later.
  • Azure CLI authenticated via the az login command.
  • An active Azure subscription and the corresponding subscription ID.
  • A resource group, or the intent to create one via Terraform.

The azurerm provider is the bridge between Terraform and the Azure Resource Manager API. The provider block defines how Terraform authenticates and interacts with the cloud account. In modern configurations, it is standard practice to explicitly define the required provider version to avoid version drift and ensure compatibility. The hashicorp/azurerm provider is the official source for this integration.

A standard provider configuration block includes the features argument, which enables support for features that would break existing infrastructure if enabled by default. Additionally, specifying the subscription_id ensures that the resources are deployed into the correct Azure subscription, which is particularly useful in multi-subtenant environments.

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

variable "subscription_id" {
description = "Azure subscription ID to deploy into"
type = string
}

provider "azurerm" {
features {}
subscriptionid = var.subscriptionid
}
```

The version constraint ~> 4.0 indicates that Terraform should use the latest version of the provider that is compatible with the 4.0 major release line. This prevents unexpected breaking changes from newer major versions of the provider while still allowing for minor and patch updates.

Basic VNet Provisioning

The most fundamental use case for azurerm_virtual_network is the creation of a single network with a defined address space and associated subnets. In Azure, a VNet is defined by one or more address spaces. The address space is a range of IP addresses in IPv4 or IPv6 that can be used to assign IP addresses to resources within the VNet.

When defining a VNet, the resource "azurerm_virtual_network" block requires specific arguments. The name argument defines the logical name of the VNet. The resource_group_name argument specifies the resource group in which the VNet will be created. The location argument specifies the Azure region where the VNet will reside. The address_space argument is a list of IP address ranges. While a VNet can have multiple address ranges, it is common practice to use a single, broad CIDR block for the VNet and carve out specific subnets from it.

In many simple implementations, subnets can be defined inline within the VNet resource using the subnet block. This approach is suitable for small networks where the relationship between the VNet and its subnets is simple and static.

```hcl
resource "azurermvirtualnetwork" "samplevnet" {
name = "samplevnet"
resourcegroupname = "DeepsLab"
location = "eastus"
address_space = ["10.0.0.0/16"]

subnet {
name = "subnet-A"
address_prefix = "10.0.1.0/24"
}
}
```

In the code above, the samplevnet resource is created in the DeepsLab resource group in the eastus region. It allocates a large address space of 10.0.0.0/16. Within this space, a single subnet named subnet-A is defined with a /24 prefix, reserving the 10.0.1.0 to 10.0.1.255 range for this specific subnet. It is important to note that when defining subnets inline, the address prefix of the subnet must fall within the address space of the VNet. Azure reserves specific IP addresses (typically the first and last few in a subnet) for internal use, such as the default gateway and DHCP services.

Advanced Subnet Configuration

For production-grade environments, subnets are often managed as independent resources rather than inline blocks. This separation allows for more granular control, easier state management, and the ability to define advanced properties such as service endpoints and delegations.

Service Endpoints

Service endpoints allow you to enable direct access from a VNet to specific Azure services, such as Storage, SQL, and Key Vault, without requiring a UDF (User Defined Firewall) rule or a NAT gateway. This is a critical feature for securing traffic to backend services. When configuring a subnet, the service_endpoints argument can be used to specify which services are accessible from that subnet.

```hcl
resource "azurermsubnet" "web" {
name = "snet-web"
resource
groupname = azurermresourcegroup.networking.name
virtual
networkname = azurermvirtualnetwork.main.name
address
prefixes = ["10.0.1.0/24"]

# Enable service endpoints for web tier
service_endpoints = [
"Microsoft.Sql",
"Microsoft.Storage"
]
}
```

In this example, the snet-web subnet is configured to allow direct connectivity to Microsoft SQL and Microsoft Storage services. This configuration ensures that traffic from the web tier to these services remains within the Azure backbone network, reducing latency and improving security by bypassing the public internet.

Subnet Delegation

Subnet delegation allows specific services to take control of a subnet. This is commonly used for services like Azure App Service, Azure Container Apps, and Azure MySQL Flexible Server. When a subnet is delegated, the service assumes responsibility for the subnet's configuration. The delegation block defines the service that will take over the subnet.

```hcl
resource "azurermsubnet" "db" {
name = "snet-db"
resource
groupname = azurermresourcegroup.networking.name
virtual
networkname = azurermvirtualnetwork.main.name
address
prefixes = ["10.0.3.0/24"]

# Delegate this subnet to a specific service
delegation {
name = "mysql-delegation"
service_delegation {
name = "Microsoft.DBforMySQL/flexibleServers"
actions = ["Microsoft.Network/virtualNetworks/subnets/join/action"]
}
}
}
```

In the code above, the snet-db subnet is delegated to the Azure MySQL Flexible Server service. The actions argument specifies the permissions that the service requires to manage the subnet. The join/action is the standard permission required for services to attach to a subnet. Once delegated, the subnet is reserved for that specific service and cannot be used for other purposes.

Dynamic Subnet Creation

As network topologies become more complex, manually defining each subnet in HCL becomes cumbersome and error-prone. Terraform allows for the dynamic creation of subnets using variables and the for_each meta-argument. This approach is particularly useful when the number of subnets or their configurations may vary between environments.

A common pattern is to define a variable that contains a map of subnet configurations. Each entry in the map represents a subnet, with properties such as the address prefix, service endpoints, and delegation details.

```hcl
variable "subnets" {
description = "Map of subnet configurations"
type = map(object({
addressprefix = string
service
endpoints = list(string)
delegation = optional(object({
name = string
service = string
actions = list(string)
}))
}))
default = {
web = {
addressprefix = "10.0.1.0/24"
service
endpoints = ["Microsoft.Sql", "Microsoft.Storage"]
}
app = {
addressprefix = "10.0.2.0/24"
service
endpoints = ["Microsoft.Sql", "Microsoft.KeyVault"]
}
}
}

resource "azurermsubnet" "dynamic" {
for
each = var.subnets
name = "snet-${each.key}"
resourcegroupname = azurermresourcegroup.networking.name
virtualnetworkname = azurermvirtualnetwork.main.name
addressprefixes = [each.value.addressprefix]
serviceendpoints = each.value.serviceendpoints

dynamic "delegation" {
for_each = each.value.delegation != null ? [each.value.delegation] : []

content {
  name = delegation.value.name
  service_delegation {
    name    = delegation.value.service
    actions = delegation.value.actions
  }
}

}
}
```

In this configuration, the for_each meta-argument iterates over the var.subnets map. For each subnet defined in the variable, a corresponding azurerm_subnet resource is created. The dynamic block allows for conditional inclusion of the delegation block, which is only applied if the subnet configuration includes delegation details. This pattern promotes DRY (Don't Repeat Yourself) code and makes it easy to add or remove subnets by simply modifying the variable definition.

Azure Virtual Network Manager

For large-scale enterprises with multiple subscriptions and management groups, managing individual VNets and peerings can become unmanageable. Azure Virtual Network Manager (VNEM) provides a centralized way to manage network topologies across resources. It allows for the creation of mesh network topologies, where all connected VNets can communicate with each other automatically.

In a typical quickstart scenario, you might deploy three VNets and use Azure Virtual Network Manager to create a mesh topology. The Terraform configuration for VNEM is more complex and involves creating a scope, a connectivity configuration, and a network topology.

```hcl

Example structure for Virtual Network Manager

Note: Specific resource names may vary based on the latest Azure documentation

resource "azurermvirtualnetworkmanager" "manager" {
name = "vnm-mesh"
resource
groupname = azurermresource_group.networking.name
scope {
# Scope definition here
}
}

resource "azurermnetworktopology" "mesh" {
name = "topology-mesh"
virtualnetworkmanagerid = azurermvirtualnetworkmanager.manager.id
network_type = "Mesh"
}

resource "azurermconnectivityconfiguration" "config" {
name = "connectivity-mesh"
virtualnetworkmanagerid = azurermvirtualnetworkmanager.manager.id
networktopologyid = azurermnetworktopology.mesh.id
connectivitymode = "Mesh"
default
virtualnetworktype = "Hub"
}
```

The deployment of VNEM via Terraform allows for the automation of complex multi-tenant networking scenarios. The connectivity_mode can be set to Mesh or Hub, depending on the desired topology. In a Mesh topology, every VNet is peered with every other VNet. In a Hub topology, all VNets are peered with a central hub VNet.

Deployment Workflow

The deployment process in Terraform follows a strict sequence of commands. Understanding this workflow is essential for safe and reliable infrastructure changes.

Initialization

The first step is to initialize the Terraform working directory. The terraform init command downloads the necessary provider plugins and sets up the backend configuration. The -upgrade parameter can be used to upgrade the provider plugins to the newest version that complies with the configuration's version constraints.

bash terraform init -upgrade

Planning

The terraform plan command creates an execution plan. It determines what actions are necessary to create or modify the configuration specified in the configuration files. This step does not execute any changes; it only previews them. This is a critical step for verifying that the intended changes match the expected outcomes before applying them to production resources.

The optional -out parameter allows you to specify an output file for the plan. This file can be used to apply the plan later, ensuring that the exact plan that was reviewed is the one that gets executed.

bash terraform plan -out main.tfplan

Applying

Once the plan is reviewed and approved, the terraform apply command executes the changes. If no plan file is specified, Terraform creates a new plan and applies it. If a plan file is specified (e.g., main.tfplan), Terraform applies that specific plan.

bash terraform apply main.tfplan

During the application process, Terraform prompts the user to confirm the changes. Typing "yes" confirms the execution. Terraform then starts creating the network resources. The output will show the progress of the creation, including the IDs of the created resources.

Outputs and Verification

To verify the deployment, it is best practice to define outputs in the Terraform configuration. Outputs allow you to retrieve information about the resources that were created, such as their names, IDs, and other properties. This is useful for downstream processes or for manual verification.

```hcl
output "resourcegroupname" {
value = azurermresourcegroup.rg.name
}

output "virtualnetworknames" {
value = azurermvirtualnetwork.vnet[*].name
}
```

After running terraform apply, the outputs will be displayed in the console. These values can be used to verify that the resources were created with the correct names and configurations. Additionally, the deployment can be verified by visiting the Virtual Networks page in the Azure Portal or by using the Azure CLI.

Comparison of Provider Versions and Features

Different versions of the azurerm provider may have different features and default behaviors. It is important to choose the appropriate version for your use case. The following table summarizes key differences between version 3.0 and 4.0, as referenced in the provided sources.

Feature Provider Version 3.0 Provider Version 4.0
Source hashicorp/azurerm hashicorp/azurerm
Version Constraint ~>3.0 ~> 4.0
Subnet Management Supports inline subnets Supports independent subnet resources
Service Endpoints Supported Supported with enhanced features
Delegation Supported Supported with enhanced features
Virtual Network Manager Limited support Full support for VNEM resources
Default Nulls Varies Explicit default = null for optional arguments

Using a newer version of the provider, such as 4.0, ensures access to the latest features and improvements, including better support for Azure Virtual Network Manager and enhanced handling of optional arguments.

Conclusion

The azurerm_virtual_network resource is a powerful tool for managing Azure networking infrastructure via Terraform. By leveraging this resource, engineers can define complex network topologies, including VNets, subnets, service endpoints, and delegations, in a code-based and repeatable manner. The ability to preview changes via terraform plan and execute them via terraform apply provides a high level of control and safety.

For advanced scenarios, Azure Virtual Network Manager offers a centralized way to manage connectivity across multiple VNets, simplifying the management of large-scale enterprise networks. By following best practices such as using version constraints, defining outputs, and utilizing dynamic resources, teams can ensure that their network infrastructure is scalable, secure, and maintainable. The integration of Terraform with Azure networking capabilities enables organizations to automate and standardize their network deployments, reducing human error and increasing efficiency. As Azure continues to evolve, the azurerm provider will continue to add support for new features, making it essential for DevOps teams to stay current with the latest provider versions and best practices.

Related Posts