Efficiently Expanding Your Azure Network: Using Azure Bicep to Add Spoke Networks to a Hub-Spoke Topology

Efficiently Expanding Your Azure Network: Using Azure Bicep to Add Spoke Networks to a Hub-Spoke Topology

The task

In this article, we will explore how to efficiently expand your Azure network by using Azure Bicep to add spoke networks to a hub-spoke topology. This comes from personal experience working with a customer who already had a hub-spoke topology in place but wished to complete their landing zone by adding another spoke-network.

Specifically, we will walk through the process of automating the creation of a spoke network in a landing zone for a customer with presence in Southeast Asia. The customer was looking to complete their landing zone in this region by adding a spoke-network to their already created hub-network.

Of course, me being one who loves to automate things, did this by using Azure Bicep, which allows me to streamline the process and ensure consistency and reliability in the network infrastructure. If they want to add more spoke-networks in the future it is as simple as deploying the code again with different parameters.

Disclaimer: I must admit I did not have the time to setup VNET-peering using Azure Bicep due to a tight deadline, so this is something I did using the Azure Portal, perhaps in the future I can update the code to include this configuration as well, and speak about it in another post.

The scope

This was a customer who already had a decent amount of network infrastructure deployed in their Azure Environment. My task was simply to create the spoke-network and connect it to their existing hub and ensure the subnets were properly configured with an existing route-table to route traffic through their network virtual appliance.

The project

First off I created a module bicep file called networking.bicep

// Parameters

param location string = resourceGroup().location
param virtualNetworkName string 
param virtualNetworkAddressPrefix string
param subnetConfiguration object
param currentDate string = utcNow('yyyy-MM-dd')
param dnsServers array

// Variables

var deploymentTags = {
  createdBy: 'Bicep Deployment'
  deploymentDate: currentDate
  workload: 'landingzone'
  environment: 'prod'
}

// Resources

// Get the resource ID of the existing route table to apply to subnet
resource subnetRouteTable 'Microsoft.Network/routeTables@2022-07-01' existing = {
  name: '%Name of route table%'
  scope: resourceGroup('%Name of resource-group%')
}

resource virtualNetworkSpoke 'Microsoft.Network/virtualNetworks@2022-07-01' = {
  name: virtualNetworkName
  location: location
  tags: deploymentTags
  properties: {
    addressSpace: {
      addressPrefixes: [
        virtualNetworkAddressPrefix
      ]
    }
    subnets: [
      {
        name: subnetConfiguration.subnets[0].name
        properties: {
          addressPrefix: subnetConfiguration.subnets[0].addressPrefix
          routeTable: {
            id: subnetRouteTable.id
          }
        }
      }
      {
        name: subnetConfiguration.subnets[1].name
        properties: {
          addressPrefix: subnetConfiguration.subnets[1].addressPrefix
          routeTable: {
            id: subnetRouteTable.id
          }
        }
      }
    ]
    dhcpOptions: {
      dnsServers: dnsServers
    }
  }
}

This will get the existing route table in the environment and save its resource ID. We want this so that we can reference it in this section of the subnet configuration:

subnets: [
      {
        name: subnetConfiguration.subnets[0].name
        properties: {
          addressPrefix: subnetConfiguration.subnets[0].addressPrefix
          routeTable: {
            id: subnetRouteTable.id
          }
        }
      }
      {
        name: subnetConfiguration.subnets[1].name
        properties: {
          addressPrefix: subnetConfiguration.subnets[1].addressPrefix
          routeTable: {
            id: subnetRouteTable.id
          }
        }
      }

subnetConfiguration is an object-parameter that contains two different subnets in this case. Both of those require the same route table object to be applied.

I use a file called main.bicep to call upon the networking module

// Parameters

@description('The location of the deployed resources')
param location string = resourceGroup().location

@description('The name of the spoke virtual network')
param virtualNetworkName string 

@description('The address prefix for the spoke virtual network')
param virtualNetworkAddressPrefix string

@description('The DNS configuration to apply in the new spoke virtual network')
param subnetConfiguration object

@description('The DNS servers that will be configured on the virtual network instead of Azure Default')
param dnsServers array

// Modules

module vnetModule 'modules/networking.bicep' = {
  name: 'deployVnet'
  params: {
    subnetConfiguration: subnetConfiguration
    virtualNetworkAddressPrefix: virtualNetworkAddressPrefix
    virtualNetworkName: virtualNetworkName
    location: location
    dnsServers: dnsServers
  }
}

This ensures we can continue to build upon this file with more modules if we have to, ensuring we don't have bicep files that become too large. Another benefit of building in modules is that you can call upon modules as you need them depending on your deployment requirements.

The final thing to do is ensuring our params.json file is updated with the correct parameters

{
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "virtualNetworkName": {
            "value": "The name of the spoke virtual network"
        },
        "virtualNetworkAddressPrefix": {
            "value": "The VNET prefix"
        },
        "subnetConfiguration": {
            "value": {
                "subnets": [
                    {
                        "name": "Name of subnet 1",
                        "addressPrefix": "IP address range of subnet 1"
                    },
                    {
                        "name": "Name of subnet 2",
                        "addressPrefix": "IP address range of subnet 1"
                    }
                ] 
            }
        },
        "dnsServers": {
            "value": [
                "DNS server 01",
                "DNS server 02"
            ]
        }
    }
}

To submit this deployment you can run the following az CLI command:

az deployment group create -g <name of RG> --template-file <path to main.bicep> --parameters <path to params.json>

Conclusion

In conclusion, this article has outlined the process of using Azure Bicep to add a spoke network to a hub-spoke topology in Azure.

By automating the creation of a spoke network in a landing zone for a customer in Southeast Asia, we were able to streamline the process and ensure consistency and reliability in the network infrastructure and we have the ability to reuse this code for any new spoke networks (I just need to fix peering as well!).

It is important to note that the code and information shared in this article is based on my personal experience working on this project and should be tested and validated in your own environment before implementation. I do not take any responsibility for any issues that may arise from using the code provided in this article in your own environment. As always, it is recommended to thoroughly test and validate any changes made to your network infrastructure before deploying to production.