Deploy a Hub Spoke Topology using Terraform

In this lab, we will create a Hub Spoke Network Topology using Terraform.

The hub network

  1. host NVA(s) and enable all the services across the vnets to communicate with each other.
  2. is connected to the 2 spoke-vnets storage and app via local peering. Each containing different services.
  3. in this exercise, we use a Azure onprem-vnet to simulate an onpremise network. It will be connect via global peering (different regions).

Exercise 1: Hub network (type constraints)

In this exercise, we dive into type contraints when specifying variables. variables allows the caller to customize. To ensure, that the customization is valid, terraform offers type contraints and validations.

Though it provides flexibility, it is good practice to follow the principles „conventions over configuration“.

Task 1: Adding hub parameter

In this task, we will learn how to implement type contraints.

  1. Via a variable hub, the user can define various properties
hub = {
 // mandatory if hub is specified.
 name                = "hub-vnet"
 // optional. If not specified, "West Europe"
 location            = "West US"
 // optional. If not specified, "rg-${hub.name}"
 resource_group_name = "rg-hub-vnet"

 // type constraint: list of address-spaces
}
  1. Within your Terraform project, define a local variable hub, which contains the correct resource_group_name.
  2. The variable hub is optional.
  3. Output local.hub to check if your logic does comply. You do not need to use any providers or modules for it to work.
  4. Run terraform init and terraform apply -auto-approve.
  5. The result should look like this
hub = {
 "address_space" = tolist([
   "10.255.0.0/16",
])
 "location" = "West Europe"
 "name" = "hub-vnet"
 "resource_group_name" = "rg-hub-vnet-hub"
}

Task 2: Creating resource group and hub vnet

  1. tf plan -out .tfplan should output this
# azurerm_resource_group.hub will be created
+ resource "azurerm_resource_group" "hub" {
   + id       = (known after apply)
   + location = "westeurope"
   + name     = "rg-hub-vnet"
}

# azurerm_virtual_network.hub will be created
+ resource "azurerm_virtual_network" "hub" {
   + address_space                  = [
       + "10.255.0.0/16",
    ]
   + dns_servers                    = (known after apply)
   + guid                           = (known after apply)
   + id                             = (known after apply)
   + location                       = "westeurope"
   + name                           = "hub-vnet"
   + private_endpoint_vnet_policies = "Disabled"
   + resource_group_name            = "rg-hub-vnet"
   + subnet                         = (known after apply)
}
  1. Run tf apply .tfplan

Task 3: Add spoke parameter

In this task, we will add a custom validation for spokes.

  1. Via a variable spoke, the user define the address range.
spokes = {
 // valid shorthand, will become 10.0.0.0/16
 onprem = "10.0"
 // valid
 app = "10.2.0.0/24"
 // valid
 storage = "10.2.1.0/28"
}
  1. A custom validation, check whether a shorthand notation is used (e.g. 10.0 will become 10.0.0.0/16) or a valid CIDR notation.
    1. validation block in variable block
    2. functions, that might be useful: alltrue, cidrhost, can, regexall.
    3. You can create a list dynamically using a for expression.
    4. Please mind the following hint from the terraform documentation for regex: If the given pattern does not match at all, the regex raises an error. To test whether a given pattern matches a string, use regexall and test that the result has length greater than zero.
  2. Within your Terraform project, define a local variable spokes, which contains the extended (valid CIDR) address ranges for all the spokes.
  3. Set the default value for spokes to
onprem  = "10.0"
app = "10.2.0.0/24"
storage = "10.2.1.0/28"
  1. Output local.hub to check if your logic does comply.
    1. You can create a map dynamically using a for expression.
  2. Create a .tfvars file and replace it content with
spokes = {
 invalid1 = "10."
 invalid2 = "A.B"
 invalid3 = "10.0.0.0/33"
 invalid4 = "10.0.0.-1/33"
}
  1. Run terraform plan -out .tfplan -var-file=.tfvars. You should see
 Invalid value in var.spokes. Use a valid CIDR (e.g. 10.2.0.0/24) or a shorthand 'A.B' that can be expanded to 'A.B.0.0/16' (e.g. 10.1).
```bash

8. Remove the `.tfvars` and run `terraform apply -auto-approve`.

9. The result should look like this


# ...
hub = {
 "address_space" = tolist([
   "10.255.0.0/16",
])
 "location" = "West Europe"
 "name" = "hub-vnet"
 "resource_group_name" = "rg-hub-vnet"
}

spokes = {
 "app" = "10.2.0.0/24"
 "onprem" = "10.0.0.0/16"
 "storage" = "10.2.1.0/28"
}

Exercise 2: Azure Firewall (dynamic blocks)

Task 1: Add Azure Firewall

  1. Extend the variable hub with a new property fw_address_prefix, which is the address_prefix for the Azure Firewall. This property is optional. Terraform should validate:
    1. var.hub.address_space has been set to 1+ address range. If not, we throw an error.
    2. If var.hub.fw_address_prefix is set, the range must be at least /26.
    3. If var.hub.fw_address_prefix is not set, check if var.hub.address_space is at least /26.
    4. functions, that might be useful: try, split, tonumber
  2. Extend the local variable for hub based on the validation above by adding the new property fw_address_prefix.
    1. functions, that might be useful: try, split, tonumber, cidrsubnet
  3. To add the firewall into a vnet, a subnet called AzureFirewallSubnet within the hub vnet is required.
    1. resource azurerm_subnet
    2. Set address_prefixes to [local.hub.fw_address_prefix]
  4. To add the firewall, a public ip is required
    1. resource azurerm_public_ip
    2. Set allocation_method to Static
    3. Set sku to Standard
  5. Add the firewall
    1. resource azurerm_firewall
    2. Set sku_tier to Standard
    3. Set sku_name to AZFW_VNet
  6. Run terraform plan -out .tfplan.
  7. Run terraform apply .tfplan.

Task 2: Add Firewall rules to enable communication

This is just a lab configuration, please don’t. open use this for production. As this basically allows everything.

  1. Use azurerm_firewall_network_rule_collection to add spokes’s address range.
  2. Use dynamic and for_each to dynamically create rule blocks for each item in local.spokes within azurerm_firewall_network_rule_collection:
    1. Set protocols to ["Any"]
    2. Set destination_ports to ["*"]
    3. Set destination_addresses to ["*"]
    4. Set source_addresses to local.spokes’s address range.
  3. Run terraform plan -out .tfplan.
  4. Run terraform apply .tfplan.

Exercise 3: Azure Spokes (modules)

In this exercise, we will create a module, that will support the following block

module "spokes" {
 source   = "./modules/spoke"
 for_each = local.spokes

 name          = each.key
 address_space = each.value
 location      = local.hub.location
 hub           = azurerm_virtual_network.hub
 nva           = azurerm_firewall.hub.ip_configuration[0]

 subnets = {
   frontend = {
     prefixes = [cidrsubnet(each.value,4,0)]
     nsg      = true
     route    = true
  }
   backend = {
     prefixes = [cidrsubnet(each.value,4,1)]
     nsg      = true
  }
   data = {
     prefixes = [cidrsubnet(each.value,4,2)]
     nsg      = true
  }
}
}
  1. A resource group rg-${var.name} will be created
  2. A vnet var.name with the given address_space will be created.
  3. Create 3 local variables
    1. subnets, which is defined subnet.name => subnet.prefixes. e.g. subnets = (frontend => [...])
    2. nsgs, array of all subnet names, that has nsg = true. e.g. nsgs = ["frontend", "backend", "data"]
    3. route, array of all subnet names, that has route = true. e.g. routes = ["frontend"]
    4. You may want to use toset function and the filtering element of the for expression.
  4. Multiple subnets with the given prefixes will be created.
    1. If route is true, route all the traffic from this subnet to the hub network.
    2. If nsg is true, create a network security group and associate it with the respective subnet.
    3. Peer the hub and spoke vnet with each other. Keep in mind, that needs to be biliteral (created in hub and created in spoke).
  5. You may need the following resource blocks: azurerm_resource_group, azurerm_virtual_network, azurerm_subnet, azurerm_network_security_group, azurerm_subnet_network_security_group_association, azurerm_virtual_network_peering, azurerm_route_table, azurerm_route, azurerm_subnet_route_table_association
  6. Run terraform plan -out .tfplan.
  7. Run terraform apply .tfplan.

Exercise 4: State management

In the previous exercise, we used a array to create 3 spokes, which is perfectly fine. But for the sake of this exercise. We will move the onprem spoke out of the array.

  1. Change the spokes default value to (remove onprem item)
app     = "10.2.0.0/24"
storage = "10.2.1.0/24"
  1. Create a new onprem.tf file in your project and paste
 module "onprem" {
  source = "./modules/spoke"

  name          = "onprem"
  address_space = "10.0.0.0/16"
  location      = "Germany West Central"
  hub           = azurerm_virtual_network.hub
  nva           = azurerm_firewall.hub.ip_configuration[0]

  subnets = {
    avd = {
      prefixes = ["10.0.1.0/24"]
      route    = true
      nsg      = true
    }
  }
}
  1. Run terraform state mv 'module.spokes["onprem"]' 'module.onprem'. This basically tells Terraform, what we just did.
  2. Run terraform plan -out .tfplan.
  3. Mind the output: Plan: 8 to add, 1 to change, 14 to destroy.. This looks like a lot of destruction. Check your code and find the value that needs to be changed, so that the output becomes Plan: 4 to add, 1 to change, 10 to destroy..
    1. The value for subnets should not be changed.
  4. Run terraform plan -out .tfplan.
  5. Run terraform apply .tfplan.

Exercise 5: Remote Backend

In this exercise, we will migrate our infrastructure state to Azure. We use a different subscription to create a storage account and to push the state to this account.

  1. Go to the Azure Portal
  2. Create a storage account tstate${your-initial} (and a resource group)
    1. using the Azure Subscription for Platform Management (e.g. dt-sub-plm-001).
    2. Set Performance to Standard.
  3. Open the storage account in the Azure Portal and create a container tfstate.
  4. Add a terraform block with a backend "azurerm" block
    1. Set subscription_id to the Azure Subscription for Platform Management used to create the storage account.
    2. Set `resource_group_name, storage_account_name, and container_name accordingly.
    3. Set key to whatever you like: plc-hubspoke.tfstate (this is the file name).
  5. Run terraform init.
  6. You should receive the following message
 Initializing the backend...
Acquiring state lock. This may take a few moments...
Do you want to copy existing state to the new backend?
  Pre-existing state was found while migrating the previous "local" backend to the
  newly configured "azurerm" backend. No existing state was found in the newly
  configured "azurerm" backend. Do you want to copy this state to the new "azurerm"
  backend? Enter "yes" to copy and "no" to start with an empty state.

  Enter a value:
  1. Enter yes and press Enter.
  2. Go to the Azure Portal and open the storage account.
    1. Locate the state file plc-hubspoke.tfstate.
    2. You should see latest state.
    3. Delete your local state files (.tfstate)

You can increase security by enabling Enable versioning for blobs under Data protection setting from the storage account amongs other features, that the storage account has to offer (soft delete, feed, etc.)

Tác giả

  • Azure
  • Infrastructure as Code
  • Terraform

Newsletter zu Aktionen

Trage dich ein um keine Aktionen von uns zu verpassen.
Wir senden 1-2 E-Mails pro Quartal.