How to Create Multiple Environments Using Terraform Workspaces - Virtual Network Creation Part 1
This post will guide you through the process of creating multiple environments in Terraform by utilizing the Terraform workspaces command. Specifically, I will demonstrate how to create two distinct environments, one for non-production and another for production, and how to utilize workspaces to keep them separate. By the end of this post, you will have a clear understanding of how to implement multiple environments in Terraform.
Requirements:
- Terraform
- Active Azure Subscription
- Storage Account
Version: Terraform v1.4.4
Terraform Tree Structure
└───vnet
│ main.tf
│ provider.tf
│ variables.tf
│
├───backend
│ backend.tf
│
├───environments
│ nonprod.tfvars
│ prod.tfvars
Create folders
To start off, let's create the folder structure. Basically, it'll look like the below
└───vnet
├───environments

Terraform Code Explanation
I'll provide an explanation of the functionality and purpose of each individual resource used in this Terraform configuration.
In the main.tf file:
azurerm_resource_group
: This resource creates an Azure resource group. A resource group is a container that holds related resources in Azure.
azurerm_virtual_network
: This resource creates an Azure virtual network. A virtual network is a logical representation of an isolated network environment in the cloud.
for_each = var.virtual_networks
loops over the elements of the variable. It will also loop over the other resources such as name, address_space, location and resource group using each.value
I've used for_each
here as I want to manage the vnet separately and independently, instead of having to manage it as a single resource using count
azurerm_subnet
: This resource creates an Azure subnet. A subnet is a range of IP addresses in a virtual network that can be used for resources such as virtual machines and load balancers.
for_each = { for subnet in var.subnets: subnet.name=> subnet }
The loop above creates a map with subnet names as keys and the entire subnet objects as values.
=> operator is used to associate the key-value pair
azurerm_network_security_group
: This resource creates an Azure network security group. A network security group is a layer of security that filters inbound and outbound traffic to an Azure resource based on user-defined rules.
In my example for NSG, I've made it more robust and easier to maintain. I've defined network_security_groups in a variable that creates a separate NSG resource for each item in the map that will correspond to the key.
Another loop is created and used to iterate through each security rule using the below loop
for rules in each.value.security_rule
I've also used a lookup in the loop e.g lookup(rules, "source_port_range",null)
- This is a function that returns the value of source_port_range if it exists and returns null if it doesn't exist. The reason why I used this is mainly the fact that in a given security rule, it can have one value or multiple values but cannot be both so it's one way to handle optional properties.
I've included most of the properties for the security rule but note, it cannot have 2 properties set e.g. source_port_ranges & source_port_range
azurerm_subnet_network_security_group_association
: This resource associates a network security group with a subnet in an Azure virtual network. It allows the network security group to filter traffic to and from resources in the subnet.
This also iterates through the subnet_id attribute. We want to associate the NSG to a subnet if it has a non-empty network_security_group_key. This is defined in my variables file under the subnet
for k, v in var.subnets
: creates 2 temporary values which represent the key and value of each item var.subnets.
for_each = { for k, v in var.subnets : k => v if v.network_security_group_key != "" }
:
is used to separate the input map from the output map
k => v
: This creates a key-value pair where the key is k
and the value is v
.
if v.network_security_group_key != ""
: This is a condition that filters out any subnets whose network_security_group_key
attribute is an empty string. In other words, if it's not empty
Create terraform files
Next, let's create the terraform files. For the vnet folder, create 3 files, main.tf, provider.tf and variables.tf
main.tf
resource "azurerm_resource_group" "this" {
name = var.resource_group_name
location = var.location
}
resource "azurerm_virtual_network" "this" {
for_each = var.virtual_networks
name = each.value.name
address_space = each.value.address_space
location = each.value.location
resource_group_name = each.value.resource_group_name
depends_on = [
azurerm_resource_group.this
]
}
resource "azurerm_subnet" "this" {
for_each = { for subnet in var.subnets : subnet.name => subnet }
name = each.value.name
resource_group_name = each.value.resource_group_name
virtual_network_name = each.value.virtual_network_name
address_prefixes = each.value.address_prefixes
depends_on = [
azurerm_virtual_network.this
]
}
resource "azurerm_network_security_group" "this" {
for_each = var.network_security_groups
name = each.key
location = var.location
resource_group_name = var.resource_group_name
security_rule = [
for rules in each.value.security_rule : {
name = rules.name
priority = rules.priority
direction = rules.direction
access = rules.access
protocol = rules.protocol
description = rules.description
destination_application_security_group_ids = rules.destination_application_security_group_ids
source_application_security_group_ids = rules.source_application_security_group_ids
source_port_range = lookup(rules, "source_port_range", null)
source_port_ranges = lookup(rules, "source_port_ranges", null)
destination_port_range = lookup(rules, "destination_port_range", null)
destination_port_ranges = lookup(rules, "destination_port_ranges", null)
destination_address_prefix = lookup(rules, "destination_address_prefix", null)
destination_address_prefixes = lookup(rules, "destination_address_prefixes", null)
source_address_prefix = lookup(rules, "source_address_prefix", null)
source_address_prefixes = lookup(rules, "source_address_prefixes", null)
}
]
}
resource "azurerm_subnet_network_security_group_association" "this" {
for_each = { for k, v in var.subnets : k => v if v.network_security_group_key != "" }
subnet_id = azurerm_subnet.this[each.key].id
network_security_group_id = azurerm_network_security_group.this[each.value.network_security_group_key].id
depends_on = [
azurerm_subnet.this,
azurerm_network_security_group.this
]
}
variables.tf
variable "network_security_groups" {
description = "A map of network security group configurations"
type = any
}
variable "subnets" {
type = map(object({
name = string
address_prefixes = list(string)
virtual_network_name = string
network_security_group_key = string
resource_group_name = string
}))
}
variable "location" {
description = "The Azure region to deploy resources in"
type = string
default = "australiaeast"
}
variable "resource_group_name" {
description = "The name of the resource group to deploy resources in"
type = string
}
variable "virtual_networks" {
description = "A map of virtual network configurations"
type = map(object({
name = string
address_space = list(string)
location = string
resource_group_name = string
}))
default = {}
}
variable "environment" {
description = "The environment to deploy to"
type = string
default = ""
}
provider.tf
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = ">= 3.0.0"
}
}
}
provider "azurerm" {
features {}
subscription_id = "subscriptionID"
tenant_id = "tenantID"
}
For the backend file, you'll need to provide the storage account name, container, key, and resource group name. You will need to create the storage account and container if you haven't already
backend.tf
terraform {
backend "azurerm" {
storage_account_name = ""
container_name = "examples-nonprod-tfstate"
key = "nonprod-terraform.tfstate"
resource_group_name = ""
}
}
And finally the environment folder. Create the nonprod.tfvars and prod.tfvars files
nonprod.tfvars
#Australia Southeast - Non Prod
location = "australiasoutheast"
resource_group_name = "terraform-examples-group2"
environment = "nonprod"
#Network
virtual_networks = {
example_vnet2 = {
name = "vnet-ause"
address_space = ["172.16.0.0/12"]
location = "australiasoutheast"
resource_group_name = "terraform-examples-group2"
}
}
subnets = {
subnet1_ause = {
name = "subnet1_ause"
address_prefixes = ["172.16.1.0/24"]
virtual_network_name = "vnet-ause"
network_security_group_key = "nsg1_ause"
resource_group_name = "terraform-examples-group2"
}
subnet2_ause = {
name = "subnet2_ause"
address_prefixes = ["172.16.2.0/24"]
virtual_network_name = "vnet-ause"
network_security_group_key = "nsg2_ause"
resource_group_name = "terraform-examples-group2"
}
}
network_security_groups = {
nsg1_ause = {
security_rule = [
{
name = "allow_http"
description = "Allow HTTP traffic"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "80"
source_address_prefix = "*"
destination_address_prefix = "10.0.1.0/24"
destination_application_security_group_ids = []
source_application_security_group_ids = []
}
]
},
nsg2_ause = {
security_rule = [
{
name = "allow_ssh"
description = "Allow SSH traffic"
priority = 110
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "22"
source_address_prefix = "*"
destination_address_prefix = "10.0.1.0/24"
destination_application_security_group_ids = []
source_application_security_group_ids = []
}
]
}
nsg3_ause = {
security_rule = [
{
name = "allow_rdp"
description = "Allow RDP traffic"
priority = 120
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "3389"
source_address_prefix = "*"
destination_address_prefix = "10.0.1.0/24"
destination_application_security_group_ids = []
source_application_security_group_ids = []
}
]
}
}
prod.tfvars
#Australia Southeast - Prod
location = "australiaeast"
resource_group_name = "terraform-examples-group"
environment = "prod"
#Network
virtual_networks = {
example_vnet = {
name = "vnet-aue"
address_space = ["10.0.0.0/16"]
location = "australiaeast"
resource_group_name = "terraform-examples-group"
}
}
subnets = {
subnet1 = {
name = "subnet1"
address_prefixes = ["10.0.1.0/24"]
virtual_network_name = "vnet-aue"
network_security_group_key = "nsg1"
resource_group_name = "terraform-examples-group"
}
subnet2 = {
name = "subnet2"
address_prefixes = ["10.0.2.0/24"]
virtual_network_name = "vnet-aue"
network_security_group_key = "nsg1"
resource_group_name = "terraform-examples-group"
}
}
network_security_groups = {
nsg1 = {
security_rule = [
{
name = "allow_http"
description = "Allow HTTP traffic"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "80"
source_address_prefix = "*"
destination_address_prefix = "10.0.1.0/24"
destination_application_security_group_ids = []
source_application_security_group_ids = []
}
]
},
nsg2 = {
security_rule = [
{
name = "allow_ssh"
description = "Allow SSH traffic"
priority = 110
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "22"
source_address_prefix = "*"
destination_address_prefix = "10.0.1.0/24"
destination_application_security_group_ids = []
source_application_security_group_ids = []
}
]
}
}
See Part 2 to apply the configuration

The full code can be found here
Found this article useful? Why not buy Phi a coffee to show your appreciation.