Featured image of post Terraform Variables Explained: Simplifying Azure Infrastructure Code

Terraform Variables Explained: Simplifying Azure Infrastructure Code

Master Terraform variables for Azure infrastructure management. Learn to declare, use, and manage variables effectively in your Infrastructure as Code projects. Discover best practices for creating flexible, reusable Terraform configurations for Azure resources.

Introduction

Hey there Everyone! đź‘‹ Welcome back to our exciting journey through the world of Infrastructure as Code (IaC) with Terraform on Azure. If you’ve been following along, you’ve already tackled setting up your environment, creating your first Azure resources, getting a handle on state management, and mastering dependencies. Seriously, awesome work so far! 🎉

Today, we’re diving into two incredibly useful features in Terraform: input and output variables. Think of them like the adjustable knobs and dials on a machine – they let you tweak and customize your infrastructure code, making it truly flexible and reusable without having to rewrite everything each time.

Building on Our Terraform Journey

Remember back in our first article when we talked about the beauty of Terraform’s declarative approach? You just describe what you want, and Terraform figures out how to build it. You saw this firsthand when you built that first VM – you defined the end state, and Terraform handled the rest.

But here’s a thought: what if you wanted to spin up that same setup but for a different environment, like testing instead of development? Or maybe you just need to change a small detail – say, the size of a virtual machine – without digging through all your configuration files, hoping you found every single place it was mentioned? That’s exactly where variables shine. They help you avoid these kinds of risky, manual edits.

Real Azure Challenges Variables Solve

These kinds of everyday headaches – like manually changing VM sizes everywhere or struggling to adapt setups for different teams or environments – are challenges that you’ve probably run into (or will soon!). I know I certainly did when I was starting out! Things like:

  1. Managing Multiple Environments: How do you keep your setups similar across development, testing, and production without just copying and pasting code everywhere (which becomes a nightmare to update)?
  2. Keeping Things Consistent: How can you make sure all your resources follow the same naming rules or have the same basic settings across your Azure subscription?
  3. Working as a Team: How do you let different team members deploy the same core infrastructure but maybe tweak a few things for their specific needs?
  4. Making Changes Safely: How do you adjust simple settings without worrying you might accidentally break your main infrastructure code?

Terraform variables offer neat solutions to these everyday problems. Instead of juggling separate configuration files for every slightly different scenario or making those risky edits we talked about, variables keep your main code structure clean and consistent, while letting you adjust the specifics easily.

In this article, we’ll walk through how to use variables in your Terraform configurations, build Azure resources that are much more flexible, and even pull out key information about what you’ve created. We’ll start simple and build up to practical examples you can use right away in your own projects.

Ready to make your Terraform code way more adaptable? Let’s dive in!

Understanding Terraform Variables

In Terraform, we have two types of variables to discuss:

  1. Input Variables: These allow you to customize your Terraform configurations without changing the main code.
  2. Output Variables: These allow you to extract and display important information about the resources you’ve created.

We’ll explore exactly what these mean and how to use them in the sections that follow.

What are Input Variables?

Input variables are like the settings you can adjust before running your Terraform configuration. They allow you to customize your infrastructure without changing your main configuration code. But what does that really mean in practice?

Say you want to provision a resource group in the Azure region “Australia East.” Normally, you’d configure it like this:

1
2
3
4
resource "azurerm_resource_group" "example" {
  name     = "my-resources"
  location = "Australia East"
}

The problem with this approach is that the region value "Australia East" is written directly into your configuration. And I get it—when you’re just starting out, it’s really tempting to hard-code the value (that is, write "Australia East" directly into the file instead of using a variable). It feels quicker and easier, right? I’ve definitely felt that pull myself.

But trust me—taking a few extra moments to use variables instead will save you so much hassle later on. You’ll absolutely thank yourself down the road.

For example, if you hard-code the region and later want to deploy to a different one, like "Australia Southeast", you’ll have to hunt down every single instance of "Australia East" and change it manually. That might be fine in a tiny project with just a few resources. But once your setup grows—dozens or even hundreds of resources—that manual change becomes slow, frustrating, and prone to errors.

This is where variables come to the rescue. Using variables, you can parameterize the Azure region. Don’t worry about the exact syntax right now—we’ll cover that in detail soon. For now, just understand that variables let you define settings in one place and reference them throughout your configuration:

1
2
3
4
5
6
7
8
9
variable "azure_region" {
  type    = string
  default = "Australia East"
}

resource "azurerm_resource_group" "example" {
  name     = "my-resources"
  location = var.azure_region
}

With this approach, if you want to deploy to Australia Southeast, you just change the region variable value to “Australia Southeast”, and all your resources will be created in the Southeast region. No need to search through your entire configuration looking for every reference to “Australia East”!

What are Output Variables?

Output variables are used to extract information about your resources after Terraform has created them. They’re particularly useful for getting data that isn’t known until the resource is actually created, such as automatically assigned IPs or resource IDs.

Let’s consider a practical example. Say you’ve created a virtual machine in Azure and you want to know what IP address Azure assigned to your virtual machine after it was created. You need this IP address because you want to use it in a network security group rule, and you won’t know this IP address until Azure actually provisions your virtual machine and assigns it an IP.

Normally, what you would do is:

  1. Deploy your VM
  2. Log into the Azure portal
  3. Navigate to the VM’s properties
  4. Copy the IP address
  5. Manually use that IP address when setting up your network security group

This manual process is time-consuming and prone to errors. Here’s where output variables come in:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
resource "azurerm_public_ip" "example" {
  name                = "vm-public-ip"
  resource_group_name = azurerm_resource_group.example.name
  location            = azurerm_resource_group.example.location
  allocation_method   = "Dynamic"
}

output "vm_public_ip" {
  value = azurerm_public_ip.example.ip_address
}

Don’t worry too much about the code above - we’ll cover the details later. Just know that with this output defined, after Terraform creates the resources, it will automatically save the IP address value in an output variable that you defined. If your later decide to use this value, you can simply reference this output variable that you’ve defined, and Terraform will provide the correct VM IP address.

You can even reference this output directly in other Terraform configurations. For example, you could use the IP address output from one Terraform project as an input to another project that sets up your security rules - all without manual copying and pasting.

Understanding Parameterization

In Terraform, variables aren’t predefined or built-in—they are what you decide to make variable. This process is called “parameterization”—taking a property that would normally be hard-coded and turning it into a parameter that can be changed without modifying your main configuration.

For example, when we turned the Azure region from a hard-coded “Australia East” into a variable, we were parameterizing that property. But how do you decide which properties to parameterize?

This depends on your specific needs and usage patterns:

  • Parameterize too much: If you turn every property into a variable, your code becomes unwieldy—requiring dozens of values to be specified before deploying anything.

  • Parameterize too little: If you don’t use enough variables, you lose the flexibility that makes Terraform powerful in the first place.

Think about ordering coffee. When you order, you specify the important customizations (variables) – size (small, medium, large), milk type (whole, oat, soy), maybe sugar or flavor shots. These are the things you likely want to change. But you don’t tell the barista the exact water temperature, the grind size of the beans, or the pressure for the espresso machine. Those details are handled by the coffee shop’s standard process (the non-variables, or hard-coded parts of your Terraform config). If you had to specify everything every time you ordered, it would take forever and be really easy to mess up!

The sweet spot is to parameterize properties that you know will change between deployments. For example:

  • Resource names that follow patterns for different environments:

    • Development: web-app-dev-rg
    • Testing: web-app-test-rg
    • Production: web-app-prod-rg
  • Azure regions you commonly deploy to:

    • Primary region: Australia East
    • Secondary region: Australia Southeast
  • VM sizes that differ between environments:

    • Development: Standard_B1s (inexpensive, low performance)
    • Testing: Standard_D2s_v3 (medium performance)
    • Production: Standard_D4s_v3 (high performance)
  • Settings that team members might need to customize:

    • Security team: Allowed IP ranges for network security groups
    • Networking team: Subnet CIDR blocks and peering configurations

As you can see, without variables, you would need to create a different copy of your Terraform code for each environment, region, VM size, and team configuration—leading to duplication and maintenance headaches.

On the other hand, if a property is always the same (like an OS disk type that’s standard across all your VMs), it probably doesn’t need to be a variable.

Why Variables are Game-Changers for Azure Infrastructure

Alright, so you’ve seen what variables are, how they save you from hard-coding values like region names or IP addresses, and you understand the idea of parameterizing the parts of your configuration that need to be flexible. That foundation is crucial!

Now, let’s really dig into why mastering variables is such a big deal. They aren’t just a minor convenience; they fundamentally transform how you manage your Azure resources in several important ways:

  1. Reusability: With variables, you can write your Terraform code once and use it multiple times. For example, instead of creating separate Terraform code for development, testing, and production environments, you use one code base and only change variables to reflect each environment’s specific needs.

  2. Collaboration: Variables enable a platform engineering approach to your infrastructure. You can define your core Terraform configuration and let different application teams use variables that suit their needs. If a team wants to deploy in a specific region or use a particular VM size, you’re giving them the flexibility to do that through variables—all while maintaining control of the underlying infrastructure design.

  3. Consistency: When you build a core configuration and use it across multiple environments with only variables changing, you ensure that all your best practices and standards are consistently applied. Whatever security controls, naming conventions, and architecture patterns you’ve defined for your development environment will carry through to testing and production. This consistency significantly reduces configuration errors and compliance issues.

  4. Simplified Maintenance: When requirements change (like needing to upgrade VM sizes or change SKUs), you can update a single variable rather than hunting through your configurations for every instance. This makes maintenance faster and less error-prone.

Real-World Example: Putting Variables to Work

Okay, theory is great, but let’s put this into practice. Imagine your team lead comes to you with a common task: “We need the infrastructure set up for our new web application. Oh, and we need it deployed in three separate environments: development, testing, and production.”

Your mind might immediately jump to the old way of doing things without variables:

  • Creating three separate Terraform configurations, probably copying and pasting a lot.
  • Manually tweaking settings like VM sizes or names in each copy.
  • Dreading the moment something needs an update because you’ll have to do it three times (and hope you don’t miss anything!).

But now, armed with your knowledge of variables, you can think differently:

  • Maintain just one set of Terraform configurations.
  • Define the specific settings for each environment (like VM sizes, counts, or naming patterns) in separate variable files.
  • Deploy to any environment easily, just by telling Terraform which variable file to use.
  • Make updates in one place, knowing they’ll apply consistently everywhere.

See the difference? In the following sections, we’ll explore exactly how to implement variables to handle this kind of multi-environment scenario smoothly, starting with the different types of input variables you can use in your Azure infrastructure code.

Types of Input Variables

Alright, we’ve seen why variables are essential. Now, let’s get practical and meet the different ‘flavors’ of input variables Terraform offers, exploring how each one helps make our infrastructure code flexible and manageable. We’ll focus on input variables first, as they’re the ones you’ll use most often when defining your infrastructure. We’ll cover output variables in more detail later in this article.

Let’s continue building our web application example and see how we can use each variable type to make our infrastructure more flexible and manageable.

String Variables

Our first variable type is the string variable. As the name implies, it holds text values. These variables are perfect for resource names, Azure regions, tags, or any property that is expressed as text.

Let’s look at how to define and use a string variable:

1
2
3
4
5
6
7
8
9
variable "azure_region" {
  type    = string
  default = "Australia East"
}

resource "azurerm_resource_group" "web_app_rg" {
  name     = "my-web-app-resource-group"
  location = var.azure_region
}

Let’s break down what’s happening here:

  1. The variable keyword tells Terraform that we’re defining a variable named “azure_region”
  2. The type = string part specifies that this variable will hold text values
  3. The default = "Australia East" sets the default value that Terraform will use unless we specifically change it
  4. In the resource group definition, for the location property, we’ve decided to use a variable instead of hardcoding “Australia East”. We tell Terraform this is a variable by using the var keyword followed by a dot and the variable name (var.azure_region). This syntax tells Terraform: “Look up the value of the azure_region variable and use it here.”

About that default value: It’s particularly useful when you have a common setting that you use most of the time. In this case, we normally deploy to Australia East, so we set it as the default. This means if we don’t explicitly change this variable, all resources will be deployed to Australia East. However, when we occasionally need to deploy to Australia Southeast, we can simply change this one variable instead of modifying our main configuration.

Note: Variables are typically stored in dedicated files (like variables.tf or .tfvars files). If we want to deploy to a different Azure region, we simply change the region value in our variable file. The next time we deploy our code, our infrastructure will be provisioned in that new region—all without changing our main configuration code. We’ll cover these variable files in more detail later in this article.

Parameterizing the region was a simple first step. What if we want to deploy our resource group to different environments and have our resource group name reflect those environments? We can add another variable:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
variable "environment" {
  type    = string
  default = "dev"
}

variable "azure_region" {
  type    = string
  default = "Australia East"
}

resource "azurerm_resource_group" "web_app_rg" {
  name     = "web-app-${var.environment}-rg"
  location = var.azure_region
}

This might look a bit more advanced with the ${var.environment} syntax, so let’s unpack it. (By the way, if this ${...} syntax looks familiar, it’s because similar interpolation patterns are used in many scripting languages and tools – quite a handy thing to use!)

What’s happening here is a technique called string interpolation. This might sound a bit fancy, but it simply means inserting variable values directly into strings. It’s telling Terraform: ‘I want my resource group name to always start with web-app-, end with -rg, and in the middle, insert the current value of my environment variable.

So if the environment variable is set to "dev" (the default), the resource group will be named "web-app-dev-rg". If we change the environment variable to "test", it becomes "web-app-test-rg", and if we set it to "prod", we get "web-app-prod-rg".

This little tweak gives us so much power that we can deploy our Terraform configuration to the appropriate environment with the appropriate naming. Without this, we would have had to create a different copy of our code for each environment—duplicating our configuration for development, testing, and production.

Number Variables

Our second input variable type is the number variable. As the name implies, you use this type to store numeric values. These are perfect for when you need to configure quantities, sizes, or counts in your Azure infrastructure.

Let’s continue with our web application example. Say we want to deploy multiple web application servers. Initially, we might want to start with two servers, but we know this number will likely change in the future as traffic grows. This is a perfect opportunity to use a variable, since we don’t want to modify our configuration each time we need to add servers.

Terraform makes it easy to create multiple identical resources using a special keyword called count. Let’s see how we combine this with our number variable to control how many servers get created:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
variable "web_app_server_count" {
  type    = number
  default = 2
}

variable "environment" { 
  # ... (rest of variables as before)
}

variable "azure_region" {
  # ... (rest of variables as before)
}

resource "azurerm_virtual_machine" "web_app_server" {
  count               = var.web_app_server_count
  name                = "web-app-server-${count.index + 1}"
  resource_group_name = "web-app-${var.environment}-rg"
  location            = var.azure_region
  
  # Other VM configuration settings would go here
}

By setting web_app_server_count to 2, we’re telling Terraform to create two identical virtual machines. If we decide to scale up to 5 servers in the future, we simply change this variable to 5 without modifying our main configuration.

Understanding Count and Unique Resource Names

When you use the count keyword, Terraform faces a challenge: it needs to create multiple resources with unique names. If all servers were simply named "web-app-server", Azure would reject the deployment because resources must have unique names within the same resource group.

To solve this problem, Terraform provides the count.index property inside resources where count is used. Here’s how that works in practice:

  1. The count = var.web_app_server_count tells Terraform to create multiple copies (let’s say 2) of this VM resource block.
  2. For each copy it processes, Terraform makes a special variable called count.index available, containing the instance number (starting from 0). (If you have some programming background, you’ll recognize this zero-based indexing – it’s very common!)
  3. We then use this count.index number within the resource block, often using string interpolation to create unique names, like: name = "web-app-server-${count.index + 1}".

So, when Terraform processes this configuration with web_app_server_count = 2:

  • For the first copy (count.index is 0), the name becomes "web-app-server-1" (because 0 + 1 = 1).
  • For the second copy (count.index is 1), the name becomes "web-app-server-2" (because 1 + 1 = 2).

We typically add 1 to count.index purely for convenience, as most people find it more natural to see resources numbered starting from 1 rather than 0.

As you can see, our configuration is becoming more powerful. The resource group name reflects the environment, the region is set by a variable, and now we can also control how many servers we deploy—all with the same configuration. This demonstrates how variables transform Terraform from a simple deployment tool into a powerful system that can manage multiple environments from a single code base.

Boolean Variables

Our third variable type is the Boolean variable. As the name implies, these variables hold simple true or false values. They’re ideal for enabling or disabling features, choosing between options, or controlling whether resources should even be created in your configuration – like simple on/off toggles.

Let’s start with a straightforward example. Imagine you want to decide whether to use a Premium or Standard storage disk for a virtual machine based on a variable.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
variable "use_premium_disk" {
  type    = bool
  description = "Set to true to use a Premium disk, false for Standard."
  default = false 
}

resource "azurerm_managed_disk" "example_disk" {
  # ... other settings like name, location, resource_group_name ...
  create_option        = "Empty"
  disk_size_gb         = 128 
  storage_account_type = var.use_premium_disk ? "Premium_LRS" : "Standard_LRS" 
}

The key part here is storage_account_type = var.use_premium_disk ? "Premium_LRS" : "Standard_LRS". This uses Terraform’s conditional expression syntax (condition ? value_if_true : value_if_false).

Think of it like a quick decision: Is it raining? (the condition: var.use_premium_disk) If yes (?), bring an umbrella (the value if true: "Premium_LRS"), otherwise (:), leave the umbrella (the value if false: "Standard_LRS").

So, in this case:

  • If var.use_premium_disk is true, the storage_account_type will be set to "Premium_LRS".
  • If var.use_premium_disk is false (the default), it will be set to "Standard_LRS".

This conditional expression is incredibly useful and you’ll see it often.

Now, let’s see a more powerful technique using boolean variables: deciding whether a resource should be created at all. A common scenario is deciding whether your web servers need public IP addresses. During development, you might want public IPs for easy testing, but in production, you might keep servers behind a load balancer with no direct public access.

Here’s how we can use a boolean variable combined with the count keyword for this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
variable "create_public_ip" {
  type    = bool
  description = "Set to true to create public IPs for the web servers."
  default = false
}

# Assuming web_app_server_count, environment, azure_region variables are also defined
# ...

resource "azurerm_public_ip" "web_server_public_ip" {
  # Use the conditional expression to set the count
  count               = var.create_public_ip ? var.web_app_server_count : 0 
  
  name                = "web-server-${count.index + 1}-pip"
  resource_group_name = "web-app-${var.environment}-rg"
  location            = var.azure_region
  allocation_method   = "Dynamic"
}

Let’s look at how this works:

  1. We define a boolean variable create_public_ip.
  2. In the azurerm_public_ip resource block, we use the same conditional expression (? :) again, but this time to set the count value: count = var.create_public_ip ? var.web_app_server_count : 0.

This expression means:

  • If create_public_ip is true, then set count to the number of web servers we’re creating (var.web_app_server_count).
  • If create_public_ip is false, then set count to 0.

This example is powerful because it uses our boolean variable not just to change a setting, but, combined with the count keyword, to decide if these public IP resources should even be created. Remember: When count is set to 0 for a resource, Terraform won’t create any instances of that resource at all. This gives us a simple yet effective way to conditionally create resources based on our boolean variable.

With this boolean toggle, we can easily enable public IPs for development environments (create_public_ip = true) and disable them for production (create_public_ip = false), all without changing our main configuration code.

This demonstrates how boolean variables, especially when combined with conditional expressions and techniques like setting count to 0, give you simple yes/no switches that can significantly change your infrastructure’s behavior.

List Variables

Our fourth variable type is the list variable. As the name implies, these variables store multiple values of the same type (like a shopping list). They’re useful when you need to work with a collection of related items - like a set of IP addresses, subnet ranges, or VM sizes.

Let’s continue with our web application example. For security reasons, we might want to restrict access to our web servers to only allow specific IP address ranges. Instead of creating separate rules for each allowed IP range, we can use a list variable:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
variable "allowed_ip_addresses" {
  type    = list(string)
  description = "List of IP CIDR ranges allowed to access the web servers on port 80."
  default = ["10.0.0.0/24", "10.0.1.0/24"] 
}

resource "azurerm_network_security_group" "web_app_nsg" {
  name                = "web-app-nsg"
  resource_group_name = "web-app-${var.environment}-rg" 
  location            = var.azure_region

  # Use dynamic block with the label "allowed_ip_rule"
  dynamic "allowed_ip_rule" { 
    for_each = var.allowed_ip_addresses 
    content {
      name                       = "Allow-Rule-Number-${allowed_ip_rule.key}" 
      priority                   = 100 + allowed_ip_rule.key 
      direction                  = "Inbound"
      access                     = "Allow"
      protocol                   = "Tcp"
      source_address_prefix      = allowed_ip_rule.value 
      destination_address_prefix = "*" # Allows traffic to any IP in the NSG's VNet
      source_port_range          = "*" # Allows traffic from any source port
      destination_port_range     = "80" # Allows traffic to port 80 (HTTP)
    }
  }
}

Let’s examine our list variable and the dynamic block in detail:

  1. We define a variable called allowed_ip_addresses with the type list(string). We use list(string) here because IP CIDR ranges like "10.0.0.0/24" are stored as text strings, even though they contain numbers they are still considered a string because it contains special characters.
  2. We populate our list with two IP ranges by default.

Understanding How to Create Rules Dynamically

You might be wondering what is this dynamic block, and why do we need it, so let’s talk about it a bit. We have our list of allowed IP addresses (var.allowed_ip_addresses). Now, how do we tell Terraform to create a separate security rule inside our Network Security Group for each IP in that list, without manually writing a security_rule block for every single one?

The key to do this without repeating code is the dynamic "allowed_ip_rule" block. Think of it as Terraform’s tool for generating multiple similar configurations automatically. Here’s how it works step-by-step in our example:

  1. dynamic "allowed_ip_rule" { ... }: This line tells Terraform, “Get ready to create multiple security rules inside this Network Security Group.” We’ve chosen the name "allowed_ip_rule" to describe what we’re doing. (The actual block type we are creating is a security_rule).
  2. for_each = var.allowed_ip_addresses: This tells Terraform, “For each item you find in the var.allowed_ip_addresses list, apply the security rule inside the content block below.”
  3. content { ... }: Inside here, we define what each generated security_rule should look like. We use temporary placeholders (allowed_ip_rule.key and allowed_ip_rule.value) that Terraform will automatically fill in for each IP address from the list.

What are allowed_ip_rule.key and allowed_ip_rule.value?

As Terraform loops through your allowed_ip_addresses list:

  • allowed_ip_rule.value: Holds the actual item from the list (the IP address value, like "10.0.0.0/24").
  • allowed_ip_rule.key: Holds the position (or the index number) of that item in the list (0 for the first IP, 1 for the second, and so on).

(Where does allowed_ip_rule. come from? It matches the name we gave the block: dynamic "allowed_ip_rule")

Putting it Together:

So, when Terraform runs for_each on our list ["10.0.0.0/24", "10.0.1.0/24"]:

  • Loop 1 (First IP):
    • The allowed_ip_rule.key value in this time is 0
    • The allowed_ip_rule.value is "10.0.0.0/24"
    • Terraform uses the content block to generate a security_rule with the above values giving us a security rule with below details:
      • name = "Allow-Rule-Number-0" (using the key=0)
      • priority = 100 (100 + key=0)
      • source_address_prefix = "10.0.0.0/24" (using the value)
  • Loop 2 (Second IP):
    • The allowed_ip_rule.key value this time is 1
    • The allowed_ip_rule.value is "10.0.1.0/24"
    • Terraform generates another security_rule with:
      • name = "Allow-Rule-Number-1" (using the key=1)
      • priority = 101 (100 + key=1)
      • source_address_prefix = "10.0.1.0/24"

This way, we automatically get a unique name and priority for each rule, and the correct source IP address is used, exactly matching the goal you described!

In Azure Network Security Groups, the priority determines the order in which rules are evaluated (lower numbers are processed first). By adding the index to a base number (100), we ensure each rule gets a unique priority.

Each rule we’re creating allows inbound TCP traffic on port 80 (standard HTTP) from the specified IP range to any destination within our network.

This approach is much more efficient than creating separate resource blocks for each security rule. Additionally, if we need to allow access from a new IP range, we simply add it to our list variable:

1
2
3
4
5
variable "allowed_ip_addresses" {
  type    = list(string)
  description = "List of IP CIDR ranges allowed to access the web servers on port 80."
  default = ["10.0.0.0/24", "10.0.1.0/24", "192.168.1.0/24"] # Added a new IP range 
}

With this single change, Terraform will automatically create an additional security rule for the new IP range, with its own name ("Allow-Rule-Number-2") and priority (102). No need to modify the rest of your configuration.

List variables provide a powerful way to manage collections of similar items, making your Terraform code more concise and easier to maintain.

Map Variables

Our fifth and final variable type is the map variable. Unlike string or number variables, the name ‘map’ might not instantly tell you what it does. A map variable simply stores pairs of related information: a unique label (we call the ‘key’) and its corresponding data (the ‘value’).

Perhaps the easiest way to picture a map variable is with an everyday example: So a map variable works exactly like the contact list on your phone. You don’t memorize phone numbers for everyone you know—you just remember their name (the key), and when you need to call someone, you look up their name and instantly get their phone number (the value).

Similarly, when your Terraform code needs to know ‘what VM size should I use in production?’, it simply looks up ‘prod’ in your environment map and immediately gets back ‘Standard_D4s_v3’—no need to write complex conditional logic or hardcode different values throughout your code.

And just like updating a contact’s information when they change their number, you only need to update a value in one place in your map when a configuration needs to change. Your code keeps using the same lookup process, but now gets the new value automatically.

Maps are perfect for grouping related settings. Just as your contact list stores more than just a name and a phone number for each person—you might also save their email address, company information, and home address—Terraform maps can store multiple related settings for each environment. For example, a map could store virtual machine size, number of instances to deploy, and whether backups should be enabled, all accessed with a single environment name as the key. We will talk about that later in this section.

Let’s continue with our web application example. We might want to use different VM sizes depending on the environment - smaller, less expensive VMs for development and testing, but more powerful VMs for production. A map variable is ideal for this scenario:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
variable "environment" {
  type    = string
  default = "dev"
}

variable "vm_sizes" {
  type = map(string)
  default = {
    "dev"  = "Standard_B1s"     # Economical size for development
    "test" = "Standard_D2s_v3"  # Medium size for testing
    "prod" = "Standard_D4s_v3"  # Larger size for production
  }
}

resource "azurerm_virtual_machine" "web_app_server" {
  count               = var.web_app_server_count
  name                = "web-app-server-${count.index + 1}"
  resource_group_name = "web-app-${var.environment}-rg"
  location            = var.azure_region
  vm_size             = var.vm_sizes[var.environment]
  
  # Other VM configuration settings would go here
}

Let’s examine how this map variable works:

  1. We define a variable called vm_sizes with type map(string), indicating a collection of key-value pairs where the values are strings.

  2. We populate our map with three entries, each representing an environment and its corresponding VM size:

    • “dev” maps to “Standard_B1s” (a budget-friendly VM size)
    • “test” maps to “Standard_D2s_v3” (a medium-performance VM size)
    • “prod” maps to “Standard_D4s_v3” (a high-performance VM size)
  3. In our virtual machine resource, we associate the appropriate VM size using: vm_size = var.vm_sizes[var.environment]

This expression matches the environment name with its corresponding VM size in our map. For example:

  • If environment = "dev", the VM size will be “Standard_B1s”
  • If environment = "prod", the VM size will be “Standard_D4s_v3”

If we didn’t have map variables, our alternatives would be less elegant:

  • We could create separate variables for each environment (like dev_vm_size, test_vm_size, prod_vm_size)
  • We could use multiple conditional expressions to select the right value
  • We might even need to maintain separate configuration files for each environment

For example, without maps, we might have to write something like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
variable "environment" {
  type    = string
  default = "dev"
}

variable "dev_vm_size" {
  type    = string
  default = "Standard_B1s"
}

variable "test_vm_size" {
  type    = string
  default = "Standard_D2s_v3"
}

variable "prod_vm_size" {
  type    = string
  default = "Standard_D4s_v3"
}

resource "azurerm_virtual_machine" "web_app_server" {
  # ...other configuration...
  vm_size = var.environment == "dev" ? var.dev_vm_size : (var.environment == "test" ? var.test_vm_size : var.prod_vm_size)
}

That’s much more cumbersome than using a map! Map variables make it much simpler to use the same configuration for multiple environments without resorting to complex expressions or duplicating code.

Map variables are especially powerful when you have multiple related settings that change together. For example, you could create a more complex map for complete environment configurations:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
variable "environment_settings" {
  type = map(object({
    vm_size = string
    instance_count = number
    enable_backups = bool
  }))
  
  default = {
    "dev" = {
      vm_size = "Standard_B1s",
      instance_count = 1,
      enable_backups = false
    },
    "prod" = {
      vm_size = "Standard_D4s_v3",
      instance_count = 3,
      enable_backups = true
    }
  }
}

Notice how in this example, for each environment we’re not just configuring VM size, but also how many instances to run and whether backups should be enabled. For development, we only need one VM instance and can skip backups to save costs. For production, we need three instances to handle the load and must enable backups for reliability.

This more advanced usage (combining maps with other data types) shows the flexibility that Terraform variables provide as you grow more comfortable with them.

With map variables, you can create highly adaptable infrastructure configurations that adjust automatically to different environments or requirements, all without changing your core resource definitions.

Input Variables: Bringing It All Together

Excellent! We’ve now explored five powerful types of input variables in Terraform:

  1. String Variables: For text values like resource names, regions, and tags
  2. Number Variables: For numeric values like counts, sizes, and thresholds
  3. Boolean Variables: For true/false switches to enable or disable features
  4. List Variables: For collections of similar items like IP ranges or subnet addresses
  5. Map Variables: For key-value groupings like environment-specific settings

Each variable type serves a specific purpose in making your Azure infrastructure code more flexible and reusable. By using the right variable type for each scenario, you’ve gained the ability to:

  • Deploy the same infrastructure to different regions with a simple variable change
  • Scale your environment up or down by adjusting a single number
  • Toggle features on and off without modifying your core configuration
  • Manage collections of related items like security rules much more easily
  • Create environment-specific configurations that adjust automatically

Throughout our web application example, we’ve seen how these variables work together to create a truly flexible infrastructure definition. With just a few variable changes, we can deploy an appropriately sized and configured web application to any environment - from a minimal setup for development to a robust, highly available configuration for production.

You might be wondering, “How do we actually change these variable values when we want to deploy to different environments?” So far, we’ve primarily used default values in our examples, but that’s just one approach. There are several ways to provide values to your Terraform variables, each suited to different situations and workflows.

In the next section, we’ll explore three common methods for supplying values to your Terraform variables when deploying your infrastructure.

Providing Values to Variables: Input Methods

Now that you know how variables work and why they’re useful, let’s look at how to provide values for them. Terraform offers several input methods, and choosing the right one depends on your workflow, project size, and how much flexibility or security you need.

Each method has its own strengths. Some are great for quick tests, while others are better suited for production environments and team collaboration. We’ll walk through the most common approaches and explore when to use each.

1. Command-Line Flags for Quick Overrides

The simplest way to assign values to variables is directly from the command line when you run Terraform.

Let’s say your configuration includes the following:

1
2
3
4
5
6
7
variable "azure_region" {
  default = "Australia East"
}

variable "web_app_server_count" {
  default = 2
}

You can override these values like this:

1
terraform apply -var="azure_region=Australia Southeast" -var="web_app_server_count=3"

This tells Terraform to deploy in Australia Southeast and spin up three servers — without changing your code.

This method is ideal during development or testing when you want to quickly try a different configuration. It keeps things simple for one-off changes.

But what happens when you need to pass ten or more values?
Writing out a long list of -var flags becomes hard to read, error-prone, and difficult to maintain — especially in CI/CD scripts or shared shell commands.

That’s where variable definition files come in.

2. .tfvars Files for Environment-Specific Values

As your project grows, you’ll often need to deploy the same Terraform configuration to multiple environments — like dev, test, and prod — with different settings for each.

Now, you might think:

Why not just change the variable values directly in main.tf before each deployment?

At first glance, that seems easy enough. But there are some real downsides:

  • You have to edit the same file every time, which slows things down and increases the chance of mistakes.
  • You lose the ability to version control environment settings separately.
  • You’re directly touching your core configuration — which undermines the whole point of using variables to make your code reusable.

Instead, Terraform gives you a better way: .tfvars files.

These are plain text files that contain just the variable values, not the logic. They keep your environment-specific data separate from your core infrastructure code.

Here’s a common project structure:

1
2
3
4
5
project/
├── main.tf              # Your Terraform resources
├── variables.tf         # Variable declarations (types and descriptions)
├── production.tfvars    # Production-specific values
└── development.tfvars   # Development-specific values

Example: production.tfvars

1
2
3
4
environment           = "prod"
azure_region          = "Australia East"
web_app_server_count  = 5
create_public_ip      = false

Example: development.tfvars

1
2
3
4
environment           = "dev"
azure_region          = "Australia Southeast"
web_app_server_count  = 1
create_public_ip      = true

To deploy to each environment, you simply pass the appropriate file:

1
2
terraform apply -var-file="development.tfvars"
terraform apply -var-file="production.tfvars"

This way, your main.tf and variables.tf stay untouched — and you avoid passing dozens of values through the command line.

3. Declaring Variables in variables.tf

In the project structure above, you might have noticed the variables.tf file. This is where you declare all the variables your configuration expects to receive.

It’s important to understand the difference between declaring a variable and assigning a value to it.

  • The variables.tf file defines what variables exist, what type they should be, and any default values or descriptions.
  • The actual values are supplied via one of the input methods: defaults, CLI flags, .tfvars files, or environment variables.

Here’s what a typical variables.tf file might look like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
variable "environment" {
  type        = string
  description = "The deployment environment (dev, test, prod)"
}

variable "azure_region" {
  type        = string
  description = "The Azure region for deployment"
}

variable "web_app_server_count" {
  type        = number
  description = "Number of web servers to deploy"
}

variable "create_public_ip" {
  type        = bool
  description = "Whether to assign public IPs to web servers"
}

This clear separation of concerns makes your configuration more maintainable:

  • main.tf defines what to deploy
  • variables.tf declares what can be configured
  • .tfvars files provide the actual values per environment

4. Using Environment Variables for Secrets

For sensitive data like passwords, tokens, or API keys, you should avoid putting values in .tfvars files — especially if they’re tracked in version control.

Terraform supports securely passing values through environment variables. Any environment variable that starts with TF_VAR_ will be treated as a Terraform variable.

For example, if your configuration includes:

1
2
3
4
variable "admin_password" {
  type      = string
  sensitive = true
}

You can set it in your shell like this:

1
2
export TF_VAR_admin_password="MySecureP@ssword123"
terraform apply

Terraform will use this value during deployment, but it won’t be printed in the output because it’s marked as sensitive.

In production environments, you’ll often configure these variables within your CI/CD pipeline — ideally after fetching secrets from a service like Azure Key Vault.

This approach keeps your sensitive data safe and your codebase clean.

Summary: Choosing the Right Input Method

Here’s a quick overview of when to use each method:

Input Method Best For
Command-line flags Quick testing, temporary overrides
.tfvars files Managing multiple environments cleanly
Environment variables Securely handling secrets and credentials
Defaults in variables.tf Sensible fallbacks for common values

By using the right input method for the right context, you make your Terraform configurations easier to use, safer to share, and far more adaptable to change.

In the next section, we’ll look at what happens when values are passed through multiple methods — and how Terraform decides which one to use.

Variable Precedence: What Takes Priority?

Now that we’ve looked at the different ways to provide variable values — from command-line flags to .tfvars files and environment variables — there’s one more important question:

What happens if Terraform receives multiple values for the same variable from different sources?

Say, for example, you’ve defined:

  • A default value in your variables.tf
  • A value for the same variable in a .tfvars file
  • And another via a command-line flag

How does Terraform know which one to use?

Terraform’s Precedence Rules

Terraform follows a clear order of precedence to resolve conflicts when the same variable is set using multiple input methods. Here’s the order from highest to lowest priority:

  1. Command-line -var flags
    Values passed directly on the command line:

    1
    
    terraform apply -var="azure_region=West US"
    
  2. Command-line -var-file flags
    Files explicitly passed with -var-file:

    1
    
    terraform apply -var-file="production.tfvars"
    
  3. Automatically loaded files
    Terraform automatically loads:

    • terraform.tfvars
    • Any file matching *.auto.tfvars
      These are picked up without needing to pass them explicitly.
  4. Environment variables
    Any shell variable starting with TF_VAR_, like:

    1
    
    export TF_VAR_azure_region="East US"
    
  5. Default values in variable declarations
    These are used only when no other value has been provided:

    1
    2
    3
    
    variable "azure_region" {
      default = "Australia East"
    }
    

Example: Who Wins?

Let’s break it down with a real example. Assume your variables.tf contains:

1
2
3
4
variable "azure_region" {
  type    = string
  default = "Australia East"
}

Then, imagine the following values are also defined for azure_region:

  • In production.tfvars:

    1
    
    azure_region = "Australia Southeast"
    
  • As an environment variable:

    1
    
    export TF_VAR_azure_region="East US"
    
  • Via command-line flag:

    1
    
    terraform apply -var="azure_region=West US"
    

Which value does Terraform use?
âś… Terraform will use “West US”, because the command-line -var flag has the highest precedence. It overrides the environment variable, the .tfvars file, and the default.

Why This Matters

Understanding precedence is crucial for avoiding surprises — especially when:

  • You’re collaborating in teams, and multiple people may provide values from different sources.
  • You’re troubleshooting a deployment that’s not using the values you expected.
  • You’re managing automated pipelines where inputs come from different stages or files.

By knowing which source takes priority, you can control how and where values are overridden — and keep your configurations predictable.

Putting It All Together: A Practical Workflow

Let’s walk through a clean and secure setup for multi-environment deployments using everything we’ve learned.

âś… Step-by-step approach:

  1. Declare your variables in variables.tf — with sensible defaults where appropriate.
  2. Store environment-specific values in .tfvars files like dev.tfvars and prod.tfvars.
  3. Pass sensitive values (like passwords) as environment variables.
  4. Deploy using a simple and repeatable command.

Example:

1
2
3
4
5
6
7
8
# Set sensitive values securely
export TF_VAR_admin_password="SecurePassword123!"

# Deploy to development
terraform apply -var-file="dev.tfvars"

# Later, deploy to production with the same code
terraform apply -var-file="prod.tfvars"

This setup gives you:

  • A single, reusable Terraform configuration
  • Clean separation between logic and data
  • Secure handling of secrets
  • Confidence that the right values are applied every time

Final Takeaway

Terraform gives you flexibility — but that also means it’s up to you to use input methods wisely. By understanding both the input options and precedence rules, you can make your infrastructure code clean, consistent, and truly environment-aware.

In the next section, we’ll look at how to structure your configuration for long-term maintainability — covering naming conventions, modular structure, and best practices.

Understanding and Using Output Variables

So far, we’ve seen how input variables make your Terraform configurations flexible by letting you customise your infrastructure before deployment. But what about the information you need after deployment — like IP addresses, resource IDs, or access keys?

That’s where output variables come in. Think of them as the receipts you get after ordering something online — they tell you what you got, where it is, and how you can use it.

Why Use Output Variables?

Output variables help you:

  1. Get important info easily – like public IPs, resource IDs, or connection strings.
  2. Avoid digging through state files – everything you need can be exposed neatly as an output.
  3. Pass data to other tools or configurations – for example, feeding an IP address into a DNS update script or another Terraform module.

Declaring an Output Variable

Here’s the basic format:

1
2
3
4
5
output "output_name" {
  value       = [resource_type.resource_name.attribute]
  description = "What this output shows"
  sensitive   = false
}

Let’s break it down:

  • output_name: What you’ll call the output when you access it later
  • value: The actual data you want to expose
  • description: (Optional) Explain what this output represents
  • sensitive: Set this to true if the data shouldn’t be shown in the terminal

Practical Examples in Azure

Let’s look at some real-world examples of using output variables with Azure resources.

Example 1: Getting the Resource Group ID

1
2
3
4
5
6
7
8
9
resource "azurerm_resource_group" "example" {
  name     = "my-resources"
  location = "Australia East"
}

output "resource_group_id" {
  value       = azurerm_resource_group.example.id
  description = "The ID of the created Resource Group"
}

This output makes it easy to reference the resource group ID in scripts, automation steps, or other Terraform modules — without hunting for it manually.

Example 2: Outputting a Public IP Address

Let’s say your virtual machine gets a dynamic IP address. You want to use it somewhere else — maybe to configure DNS or a firewall rule.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
resource "azurerm_public_ip" "example" {
  name                = "vm-public-ip"
  resource_group_name = azurerm_resource_group.example.name
  location            = azurerm_resource_group.example.location
  allocation_method   = "Dynamic"
}

output "vm_public_ip" {
  value       = azurerm_public_ip.example.ip_address
  description = "The public IP address of the VM"
}

Once applied, you can view the output using:

1
terraform output vm_public_ip

Need to use it in a script?

1
VM_IP=$(terraform output -raw vm_public_ip)

Example 3: Handling Sensitive Outputs

Some data — like access keys or passwords — should never be shown in plain text.

1
2
3
4
5
output "storage_account_key" {
  value       = azurerm_storage_account.example.primary_access_key
  description = "Primary access key for the storage account"
  sensitive   = true
}

When sensitive is set to true, Terraform hides the value in the CLI output. You can still use it programmatically, but it won’t show up in logs — which is exactly what you want in CI/CD pipelines.

When and Why to Use Outputs

Here’s where output variables become especially useful:

  • Sharing data between modules
    A child module can expose key outputs (like IPs or names), and the root module can pick them up.

  • Passing info to later deployment stages
    Say your pipeline needs to register a DNS entry after a VM is created. Use the output IP directly.

  • Making your infrastructure self-documenting
    With well-named outputs and good descriptions, your Terraform code becomes easier for teammates to read and understand — even if they’ve never touched Terraform before.

Output Best Practices

  • âś… Use clear, descriptive names for each output
    Avoid output1 or rg_id. Instead use resource_group_id.

  • âś… Include descriptions to explain what each output is for
    This helps you (and your teammates) understand their purpose at a glance.

  • âś… Mark sensitive data with sensitive = true
    Keep secrets safe — even from your terminal scrollback buffer.

  • đźš« Avoid outputting too much
    Only expose values you’ll actually need. Outputs should be useful, not noisy.

By using outputs wisely, you make your Terraform projects not just work, but also communicate. That’s especially powerful when you’re collaborating with others or building tools that depend on Terraform.

In the next section, we’ll dive into best practices for variables — wrapping up with some practical wisdom to help you avoid pitfalls and keep your configurations clean and maintainable.

Best Practices for Using Terraform Variables

Now that you understand how input and output variables work, let’s focus on how to use them well. Variables can either bring structure and flexibility to your Terraform code — or introduce confusion if used carelessly. These best practices will help you write Terraform configurations that are clean, maintainable, and easy for your team to understand and extend.

1. Use Clear, Descriptive Names

Good variable names act like labels on buttons: they should make it immediately obvious what each variable controls.

  • Stick with lowercase letters and underscores (_) for readability.
  • Be specific without being overly long.
  • Consider adding a prefix or suffix if the variable relates to a specific resource or scope (like db_password, web_app_location, or vm_size).

Examples:

1
2
3
4
5
6
7
8
9
# Clear and descriptive
variable "primary_azure_region" {
  type = string
}

# Avoid generic names like this:
variable "r" {
  type = string
}

2. Add Descriptions to Every Variable

Terraform lets you add a description to each variable — use it. A few extra words now can save future-you (or a teammate) from guessing what a variable is for, especially in larger configurations.

1
2
3
4
5
variable "vm_size" {
  type        = string
  default     = "Standard_D2s_v3"
  description = "The size of the Azure VM. Affects compute capacity and pricing."
}

The same goes for outputs — always include a description explaining what the output represents and why it’s useful.

3. Use Variable Validation

If you know a variable should only accept certain values, use validation blocks to enforce that. This avoids subtle bugs and catches configuration issues early, before Terraform even starts planning resources.

1
2
3
4
5
6
7
8
9
variable "environment" {
  type        = string
  description = "The deployment environment (dev, test, prod)"

  validation {
    condition     = contains(["dev", "test", "prod"], var.environment)
    error_message = "Environment must be 'dev', 'test', or 'prod'."
  }
}

4. Provide Defaults Thoughtfully

Default values are great for simplifying usage, but not every variable should have one. Use defaults for things like regions or naming prefixes — values that are unlikely to change often. Avoid setting defaults for sensitive or environment-specific values like passwords, instance counts, or pricing tiers.

When your configurations grow, group related variables together to stay organised. You can do this within a single variables.tf file, or split them into multiple files (e.g., networking-variables.tf, compute-variables.tf, etc.) to mirror your configuration’s structure.

6. Use Variables to Enforce Naming Conventions

You can simplify and standardise naming across resources using variables and string interpolation:

1
2
3
4
resource "azurerm_resource_group" "example" {
  name     = "${var.project_name}-${var.environment}-rg"
  location = var.azure_region
}

This way, names will always follow your conventions — whether you’re deploying to dev, test, or prod.

7. Handle Sensitive Data Securely

Never hard-code sensitive values like passwords or API keys in your Terraform files. Instead:

  • Use environment variables locally during development.
  • Store secrets in Azure Key Vault (or a similar tool) and fetch them securely via automation.
  • Mark sensitive variables and outputs using sensitive = true to prevent accidental exposure in logs or CLI output.
1
2
3
4
5
variable "db_password" {
  type        = string
  sensitive   = true
  description = "Database password. Should be provided via environment variable."
}

If you have multiple values that vary together — like VM size, count, and backup policy by environment — use a map variable instead of separate variables or conditionals.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
variable "environment_settings" {
  type = map(object({
    vm_size        = string
    instance_count = number
    enable_backups = bool
  }))

  default = {
    dev = {
      vm_size        = "Standard_B1s"
      instance_count = 1
      enable_backups = false
    },
    prod = {
      vm_size        = "Standard_D4s_v3"
      instance_count = 3
      enable_backups = true
    }
  }
}

This keeps your configuration clean and flexible — no need to rewrite logic for each environment.

9. Be Selective with Output Variables

Only expose outputs that are genuinely useful to the user or downstream systems. Avoid outputting internal values that aren’t needed, as this can clutter the terminal output and make debugging harder.

10. Keep Your Configurations DRY (Don’t Repeat Yourself)

Repetition often signals an opportunity to use a variable. If you find yourself typing the same region name, VM size, or prefix multiple times, that’s a perfect candidate for a variable.

It’s not just about saving keystrokes — reducing duplication makes it easier to update your configuration without missing something.

11. Use .tfvars Files for Environment-Specific Values

Keep your core configuration the same across environments (dev, test, prod) by passing in different values via .tfvars files:

1
2
terraform apply -var-file="dev.tfvars"
terraform apply -var-file="prod.tfvars"

This helps you:

  • Avoid modifying code between deployments
  • Keep environments consistent
  • Reduce risk of accidental misconfiguration

Wrapping Up

Following these best practices will help you write Terraform configurations that are:

  • Flexible, with variables that can adapt across environments and use cases
  • Safe, by separating sensitive data and avoiding risky defaults
  • Maintainable, with clear naming, documentation, and structure
  • Reusable, letting you scale your infrastructure efficiently without duplicating code

When used thoughtfully, variables become more than just placeholders — they turn your Terraform code into a dynamic, professional-grade infrastructure blueprint.

Conclusion

We’ve covered a lot of ground in this guide — and if you’ve followed along, you’re now much more confident working with Terraform variables in real Azure infrastructure projects.

Variables might seem like a small part of Terraform at first glance, but as you’ve seen, they’re at the heart of making your configurations adaptable, secure, and reusable. They give your code the flexibility to scale across teams, environments, and scenarios — without sacrificing structure or maintainability.

What We’ve Accomplished

Here’s a quick recap of what you’ve learned:

  1. How input variables make your Terraform code customisable and portable
  2. The five core types of input variables — string, number, boolean, list, and map — and when to use each
  3. How to assign variable values using defaults, CLI flags, .tfvars files, or environment variables
  4. How output variables help you extract useful information from your deployed infrastructure
  5. Best practices for writing clean, secure, and easy-to-maintain variable configurations

This knowledge gives you the tools to build infrastructure that’s not only technically sound, but also scalable and team-friendly.

Bringing It All Together

By using variables effectively, you can:

  • Maintain a single codebase that adapts across dev, test, and prod environments
  • Keep sensitive data out of your Terraform files and version control
  • Apply consistent naming, resource sizing, and policies across teams
  • Extract critical values (like IPs or access keys) for use in pipelines, scripts, or documentation
  • Build infrastructure that integrates smoothly with broader workflows and tooling

And best of all — you’ve gained the ability to turn one-off deployments into a repeatable, professional-grade Infrastructure as Code system.

What’s Next: Reusability at Scale

In the next and final article in this series, we’ll explore Terraform Modules — the real game-changer for reusability and team collaboration.

Modules allow you to group resources into logical, reusable units — almost like writing functions in code. You’ll be able to build once and deploy anywhere, consistently and confidently.

We’ll also wrap things up with a full-scale project that pulls together everything you’ve learned so far — variables, outputs, dependencies, state management, and modules — into a production-ready, multi-tier Azure architecture.

It’s a hands-on, real-world application of everything we’ve covered, and a great way to solidify your skills.

Parting Thoughts

If you’ve made it this far, take a moment to appreciate what you’ve accomplished. You’ve gone from manually defining Azure resources to building flexible, environment-aware infrastructure powered by variables — and you’re just getting started.

Thanks for sticking with the journey so far. I’ll see you in the next article, where we’ll take Terraform to the next level with modules and real-world architecture patterns.