You are currently viewing Terraform on Azure Part 1 – Hub and Spoke Network

Terraform on Azure Part 1 – Hub and Spoke Network

Why Terraform, and why IaC?​

Terraform is a tool created by Hashicorp that allows you to provision infrastructure to many different type of providers (Azure, AWS, GCP, DigitalOcean) by writing code. This post is the first in a series of posts that will dive into provisioning Terraform with Azure Devops Service, and building our enterprise landing zone from the ground up.

Terraform has been around for a few years now and has seemingly become the gold standard of IaC tooling used by organizations worldwide. For good reason, it has excellent readability, contains integrations to most public/private cloud providers allowing multi-cloud deployments.

There are multiple benefits of Infrastructure as Code, such as. Reusability, once you have written out the environment you can destroy/rebuild this on the fly over and over again. The single source of truth concept, you can achieve a level of standardization among deployments using IaC and deploying via a pipeline.

Prerequisites before we get started.

Terraform downloaded and installed either by a package manager (Chocolatey) or have manually setup the environmental variables. To properly test and verify this is working is to open a terminal, and type terraform –version.
An Azure Subscription.
AZ CLI installed.
Visual Studio Code with Terraform extension installed.

Create your remote state file with Azure storage account.

First off what is the remote state file? Well basically it is a shared file that holds the environment state as Terraform is aware of it. This is very important when you wish to run say a Terraform Apply command and not deleting an existing VM or storage account. I have created my storage account useastnetworktfstate, for this purpose.

Directory Structure

The directory structure for this will look as follows, I will subsequently dive into each object in this directory as we move along with this post.

Azure_HubAndSpoke_folder

    └──main.tf

    └──providers.tf

    └──terraform.tfvars

  • main.tf – The resources that we are deploying into Azure.
  • providers.tf – The variables that resources depend on for configuration. 
  • network.tfvars – Contains shared resources that will be utilized across multiple deployments. 

Terraform Directory Analysis

main.tf

Defines the Azure Resources that we are deploying.

backend "azurerm" {

      storage_account_name = "useastnetworktfstate"

      resource_group_name = "useast-network-remotestate"

      container_name = "tfstate"

      key = "terraform.tfstate"

  }

}

variable "resource_group_name" {

  type = string

}

variable "resource_location" {

  type = string

}

 

variable "azurerm_hub_network_name" {

  type = string

}

variable "azurerm_hub_network_address" {

  type = string

}

variable "azurerm_hub2spoke1_peername" {

  type = string

}

variable "azurerm_hub2spoke2_peername" {

  type = string

}

variable "azurerm_spoke1_network_name" {

  type = string

}

variable "azurerm_spoke1_network_address" {

  type = string

}

variable "azurerm_spoke1_subnet1_name" {

  type = string

}

variable "azurerm_spoke1_subnet1_address" {

  type = string

}

variable "azurerm_hub_subnet1_name" {

  type = string

}

variable "azurerm_hub_subnet1_address" {

  type = string

}

variable "azurerm_spoke1tohub_peername" {

  type = string

}

variable "azurerm_spoke2_network_name" {

  type = string

}

variable "azurerm_spoke2_network_address" {

  type = string

}

variable "azurerm_spoke2_subnet1_name" {

  type = string

}

variable "azurerm_spoke2_subnet1_address" {

  type = string

}

variable "azurerm_spoke2tohub_peername" {

  type = string

}

resource "azurerm_resource_group" "useast-network-rg" {

    name = var.resource_group_name

    location = var.resource_location

}

resource "azurerm_virtual_network" "useast-hub-vnet" {

  name = "${var.azurerm_hub_network_name}"

  address_space       = ["${var.azurerm_hub_network_address}"]

  resource_group_name = var.resource_group_name

  location            = var.resource_location

  depends_on = [

    azurerm_resource_group.useast-network-rg

  ]

}

resource "azurerm_virtual_network_peering" "hub_to_spoke1" {

  name = "${var.azurerm_hub2spoke1_peername}"

  resource_group_name = var.resource_group_name

  virtual_network_name = var.azurerm_hub_network_name

  remote_virtual_network_id = azurerm_virtual_network.useast-spoke1-vnet.id

  allow_virtual_network_access = true

  allow_forwarded_traffic = true

  depends_on = [

    azurerm_resource_group.useast-network-rg,

    azurerm_virtual_network.useast-hub-vnet,

    azurerm_virtual_network.useast-spoke1-vnet

  ]

}

resource "azurerm_virtual_network_peering" "hub_to_spoke2" {

  name = "${var.azurerm_hub2spoke2_peername}"

  resource_group_name = var.resource_group_name

  virtual_network_name = var.azurerm_hub_network_name

  remote_virtual_network_id = azurerm_virtual_network.useast-spoke2-vnet.id

  allow_virtual_network_access = true

  allow_forwarded_traffic = true

  depends_on = [

    azurerm_resource_group.useast-network-rg,

    azurerm_virtual_network.useast-hub-vnet,

    azurerm_virtual_network.useast-spoke2-vnet

  ]

}

resource "azurerm_subnet" "useast-hub-subnet1" {

  name                 = var.azurerm_hub_subnet1_name

  address_prefix     = var.azurerm_hub_subnet1_address

  virtual_network_name = var.azurerm_hub_network_name

  resource_group_name  = var.resource_group_name

  depends_on = [

   azurerm_virtual_network.useast-hub-vnet,

  ]

}

resource "azurerm_virtual_network" "useast-spoke1-vnet" {

    name = var.azurerm_spoke1_network_name

    address_space       = [var.azurerm_spoke1_network_address]

    resource_group_name = var.resource_group_name

    location            = var.resource_location

    depends_on = [

    azurerm_resource_group.useast-network-rg

  ]

}

resource "azurerm_subnet" "useast-spoke1-subnet1" {

  name                 = var.azurerm_spoke1_subnet1_name

  address_prefix     = "${var.azurerm_spoke1_subnet1_address}"

  virtual_network_name = var.azurerm_spoke1_network_name

  resource_group_name  = var.resource_group_name

  depends_on = [

    azurerm_virtual_network.useast-spoke1-vnet,

    azurerm_resource_group.useast-network-rg

  ]

}

resource "azurerm_virtual_network_peering" "spoke1_to_hub" {

  name = var.azurerm_spoke1tohub_peername

  resource_group_name = var.resource_group_name

  virtual_network_name = var.azurerm_spoke1_network_name

  remote_virtual_network_id = azurerm_virtual_network.useast-hub-vnet.id

  allow_forwarded_traffic = true

  allow_virtual_network_access = true

  depends_on = [

    azurerm_resource_group.useast-network-rg,

    azurerm_virtual_network.useast-hub-vnet,

  ]

}

resource "azurerm_virtual_network" "useast-spoke2-vnet" {

    name = var.azurerm_spoke2_network_name

    address_space       = [var.azurerm_spoke2_network_address]

    resource_group_name = var.resource_group_name

    location            = var.resource_location

    depends_on = [

    azurerm_resource_group.useast-network-rg

  ]

}

resource "azurerm_subnet" "useast-spoke2-subnet1" {

  name                 = var.azurerm_spoke2_subnet1_name

  address_prefix     = "${var.azurerm_spoke2_subnet1_address}"

  virtual_network_name = var.azurerm_spoke2_network_name

  resource_group_name  = var.resource_group_name

  depends_on = [

    azurerm_virtual_network.useast-spoke2-vnet,

    azurerm_resource_group.useast-network-rg

  ]

}

resource "azurerm_virtual_network_peering" "spoke2_to_hub" {

  name = var.azurerm_spoke2tohub_peername

  resource_group_name = var.resource_group_name

  virtual_network_name = var.azurerm_spoke2_network_name

  remote_virtual_network_id = azurerm_virtual_network.useast-hub-vnet.id

  allow_forwarded_traffic = true

  allow_virtual_network_access = true

  depends_on = [

    azurerm_resource_group.useast-network-rg,

    azurerm_virtual_network.useast-hub-vnet,

    azurerm_virtual_network.useast-spoke2-vnet

  ]

}

Now, obviously there is quite a lot of meat in the main.tf file so let me break it down for you. As this is the primary file, it makes sense as to why there is a lot going on here. 

backend – This function is defining our Azure RM backend state file, which is in a private container within the storage account mentioned above. 

variables – These are being defined as strings within the main.tf file, however these values are being referenced at the terraform.tfvars file inherently.

resources – Resource Group, Hub Virtual Network, 2 Spoke Virtual Networks.

terraform.tfvars

resource_group_name = "useast-network-rg"
resource_location = "eastus"
azurerm_hub_network_name = "useast-hub-vnet"
azurerm_hub_network_address = "10.0.0.0/16"
azurerm_hub2spoke1_peername = "hub_to_spoke1"
azurerm_hub_subnet1_name = "usnc-hub-subnet1"
azurerm_hub_subnet1_address = "10.0.1.0/24"
azurerm_spoke1_network_name = "useast-spoke1-vnet"
azurerm_spoke1_network_address = "10.1.0.0/16"
azurerm_spoke1_subnet1_name = "useast-spoke1-subnet1"
azurerm_spoke1_subnet1_address = "10.1.1.0/24"
azurerm_spoke1tohub_peername = "spoke1_to_hub"
azurerm_spoke2_network_name = "useast-spoke2-vnet"
azurerm_spoke2_network_address = "10.2.0.0/16"
azurerm_spoke2_subnet1_name = "useast-spoke1-subnet1"
azurerm_spoke2_subnet1_address = "10.2.1.0/24"
azurerm_spoke2tohub_peername = "spoke2_to_hub"
azurerm_hub2spoke2_peername = "hub_to_spoke2"

The values within this file are pretty self-explanatory, instead of manually typing these values over and over again. We simply reference these values with the variables function.

Terraform will default to use a terraform.tfvars file, however if you wish you pass one named something different you can. All you must do is pass the -var-file=”filename.tfvars” command when you run terraform plan/apply. 

providers.tf

provider "azurerm" {

    version = "2.5.0"

    features {}

}

Terraform Deployment

Now it is time to deploy this, firstly we must connect against our Azure subscription. 

az login

az account set –subscription Teds-DevEssential-Sub

Sweet, we are now connected to our Azure subscription, resources can now be deployed using Terraform. Let’s first run the Terraform Init this will get our backend configuration setup. 

Next, lets run terraform plan this will run through exactly what this code will deploy in Azure for us.



Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create




Terraform will perform the following actions:




  # azurerm_resource_group.useast-network-rg will be created
  + resource "azurerm_resource_group" "useast-network-rg" {
      + id       = (known after apply)
      + location = "eastus"
      + name     = "useast-network-rg"
    }




  # azurerm_subnet.useast-hub-subnet1 will be created
  + resource "azurerm_subnet" "useast-hub-subnet1" {
      + address_prefix                                 = "10.0.1.0/24"
      + enforce_private_link_endpoint_network_policies = false
      + enforce_private_link_service_network_policies  = false
      + id                                             = (known after apply)
      + name                                           = "usnc-hub-subnet1"
      + resource_group_name                            = "useast-network-rg"
      + virtual_network_name                           = "useast-hub-vnet"
    }




  # azurerm_subnet.useast-spoke1-subnet1 will be created
  + resource "azurerm_subnet" "useast-spoke1-subnet1" {
      + address_prefix                                 = "10.1.1.0/24"
      + enforce_private_link_endpoint_network_policies = false
      + enforce_private_link_service_network_policies  = false
      + id                                             = (known after apply)
      + name                                           = "useast-spoke1-subnet1"
      + resource_group_name                            = "useast-network-rg"
      + virtual_network_name                           = "useast-spoke1-vnet"
    }




  # azurerm_subnet.useast-spoke2-subnet1 will be created
  + resource "azurerm_subnet" "useast-spoke2-subnet1" {
      + address_prefix                                 = "10.2.1.0/24"
      + enforce_private_link_endpoint_network_policies = false
      + enforce_private_link_service_network_policies  = false
      + id                                             = (known after apply)
      + name                                           = "useast-spoke1-subnet1"
      + resource_group_name                            = "useast-network-rg"
      + virtual_network_name                           = "useast-spoke2-vnet"
    }




  # azurerm_virtual_network.useast-hub-vnet will be created
  + resource "azurerm_virtual_network" "useast-hub-vnet" {
      + address_space       = [
          + "10.0.0.0/16",
        ]
      + id                  = (known after apply)
      + location            = "eastus"
      + name                = "useast-hub-vnet"
      + resource_group_name = "useast-network-rg"




      + subnet {
          + address_prefix = (known after apply)
          + id             = (known after apply)
          + name           = (known after apply)
          + security_group = (known after apply)
        }
    }




  # azurerm_virtual_network.useast-spoke1-vnet will be created
  + resource "azurerm_virtual_network" "useast-spoke1-vnet" {
      + address_space       = [
          + "10.1.0.0/16",
        ]
      + id                  = (known after apply)
      + location            = "eastus"
      + name                = "useast-spoke1-vnet"
      + resource_group_name = "useast-network-rg"




      + subnet {
          + address_prefix = (known after apply)
          + id             = (known after apply)
          + name           = (known after apply)
          + security_group = (known after apply)
        }
    }




  # azurerm_virtual_network.useast-spoke2-vnet will be created
  + resource "azurerm_virtual_network" "useast-spoke2-vnet" {
      + address_space       = [
          + "10.2.0.0/16",
        ]
      + id                  = (known after apply)
      + location            = "eastus"
      + name                = "useast-spoke2-vnet"
      + resource_group_name = "useast-network-rg"




      + subnet {
          + address_prefix = (known after apply)
          + id             = (known after apply)
          + name           = (known after apply)
          + security_group = (known after apply)
        }
    }




  # azurerm_virtual_network_peering.hub_to_spoke1 will be created
  + resource "azurerm_virtual_network_peering" "hub_to_spoke1" {
      + allow_forwarded_traffic      = true
      + allow_gateway_transit        = (known after apply)
      + allow_virtual_network_access = true
      + id                           = (known after apply)
      + name                         = "hub_to_spoke1"
      + remote_virtual_network_id    = (known after apply)
      + resource_group_name          = "useast-network-rg"
      + use_remote_gateways          = (known after apply)
      + virtual_network_name         = "useast-hub-vnet"
    }




  # azurerm_virtual_network_peering.hub_to_spoke2 will be created
  + resource "azurerm_virtual_network_peering" "hub_to_spoke2" {
      + allow_forwarded_traffic      = true
      + allow_gateway_transit        = (known after apply)
      + allow_virtual_network_access = true
      + id                           = (known after apply)
      + name                         = "hub_to_spoke2"
      + remote_virtual_network_id    = (known after apply)
      + resource_group_name          = "useast-network-rg"
      + use_remote_gateways          = (known after apply)
      + virtual_network_name         = "useast-hub-vnet"
    }




  # azurerm_virtual_network_peering.spoke1_to_hub will be created
  + resource "azurerm_virtual_network_peering" "spoke1_to_hub" {
      + allow_forwarded_traffic      = true
      + allow_gateway_transit        = (known after apply)
      + allow_virtual_network_access = true
      + id                           = (known after apply)
      + name                         = "spoke1_to_hub"
      + remote_virtual_network_id    = (known after apply)
      + resource_group_name          = "useast-network-rg"
      + use_remote_gateways          = (known after apply)
      + virtual_network_name         = "useast-spoke1-vnet"
    }




  # azurerm_virtual_network_peering.spoke2_to_hub will be created
  + resource "azurerm_virtual_network_peering" "spoke2_to_hub" {
      + allow_forwarded_traffic      = true
      + allow_gateway_transit        = (known after apply)
      + allow_virtual_network_access = true
      + id                           = (known after apply)
      + name                         = "spoke2_to_hub"
      + remote_virtual_network_id    = (known after apply)
      + resource_group_name          = "useast-network-rg"
      + use_remote_gateways          = (known after apply)
      + virtual_network_name         = "useast-spoke2-vnet"
    }




Plan: 11 to add, 0 to change, 0 to destroy.

}

As you can see, this will deploy a Hub Virtual Network along with a two separate spoke virtual networks peered to the hub virtual network. As well my specified resource group, useast-network-rg will be created. This will begin our landing zone as the next post in this series will be implementing Azure Bastion for secure remote management along with Azure Firewall to steer traffic.

I know this was a bit lengthy of a post however I hope it has been informative, please join me as I continue this series. 

Thanks,
Ted

Leave a Reply