Featured image of post Your First Terraform Project on Azure: A Step-by-Step Guide

Your First Terraform Project on Azure: A Step-by-Step Guide

Learn how to create, modify, and destroy Azure resources using Terraform in this comprehensive guide. Perfect for beginners, this step-by-step tutorial covers the full lifecycle of infrastructure as code on Azure.

Introduction

Welcome back Everyone to our Terraform on Azure series! If you’ve been following along, we’re now ready to take an important step forward - creating your very first full-fledged Terraform project on Azure.

Where We’ve Been

Let’s quickly recap our journey so far:

  1. In our first article, we explored the fundamentals of Terraform and why it’s a game-changer for managing Azure infrastructure.
  2. In the second article, we set up our local Terraform environment, including Azure CLI, Terraform installation, and Visual Studio Code.

If you haven’t read these articles yet, you might want to review them to make sure you’re all set up and ready to go.

What We’re Building Today

Today, we’re moving beyond the basics and building an actual Azure infrastructure using Terraform. We’ll create several interconnected resources:

  • A Resource Group to keep everything organized
  • A Virtual Network to provide connectivity
  • A Subnet to segment our network
  • A Network Security Group to secure our resources
  • A Virtual Machine where we can run our workloads

By the end of this guide, you’ll have a fully functional Azure environment, created and managed with Terraform.

What You’ll Learn

This hands-on project will teach you:

  • How to plan your Azure infrastructure before writing any code
  • How to define multiple, interconnected Azure resources in Terraform
  • The full Terraform workflow – from initialization to destruction
  • How to verify your Azure resources and make changes to your infrastructure

Why This Matters

Creating a multi-resource project is a significant step up from managing single resources. This project gives you a taste of real-world infrastructure management, preparing you for more complex scenarios you’ll encounter in your cloud journey.

So, are you ready to level up your Terraform skills? Let’s dive in!

Planning Our Azure Infrastructure

Before we start writing any Terraform code, it’s important to plan out our Azure infrastructure. A clear plan helps us understand what we’re building and how everything fits together.

Our Infrastructure Blueprint

We’re going to create a basic but realistic Azure environment with these key components:

  1. Resource Group - A container that keeps all our Azure resources neatly organized
  2. Virtual Network (VNet) - Our own private network in Azure that provides the networking foundation
  3. Subnet - A range of IP addresses within our VNet that helps organize network traffic
  4. Network Security Group (NSG) - Our virtual firewall that controls what traffic can flow in and out
  5. Virtual Machine (VM) - The compute resource where we’ll run our applications

These components work together in a hierarchy:

  • The Resource Group contains everything
  • Inside it, the Virtual Network establishes our network boundaries
  • The Subnet carves out a specific IP range within the VNet
  • The Network Security Group attaches to the subnet to filter traffic
  • The VM connects to the subnet through a network interface

Here’s a simple diagram of this structure:

1
2
3
4
5
6
7
8
9
Resource Group
└─── Virtual Network
     └─── Subnet
          ├─── Network Security Group
          └─── Virtual Machine

This architecture provides several benefits:

  • Organization: Logical grouping of related resources
  • Security: Network isolation and traffic filtering
  • Manageability: Simplified resource administration
  • Scalability: A foundation that can grow with your needs

Planning for Terraform

As we prepare to implement this in Terraform, we’ll need to make decisions about:

  • Resource naming conventions and locations
  • Network address spaces (10.0.0.0/16 for VNet, 10.0.1.0/24 for subnet)
  • Security rules for our NSG (we’ll allow RDP access)
  • VM specifications (size, OS image, credentials)

Don’t worry if this seems complex—we’ll implement each component step by step in our Terraform configuration.

With our blueprint established, let’s start building.

Setting Up Our Terraform Configuration

Now it’s time to start setting up our Terraform configuration. We’ll create a new project directory and initialize our Terraform workspace.

Creating the Project Directory

First, let’s create a new directory for our project:

  1. Open your terminal or command prompt.
  2. Navigate to where you want to create your project.
  3. Create a new directory and move into it:
1
2
mkdir azure-terraform-project
cd azure-terraform-project

You’ve just created a home for your Terraform project.

Creating Our Main Configuration File

Now, let’s create our main Terraform configuration file:

  1. In your project directory, create a new file named main.tf.
  2. Open this file in your favorite text editor (Visual Studio Code is a great choice, as we set it up in the previous article).

Configuring the Azure Provider

In our main.tf file, we’ll start by configuring the Azure provider. This tells Terraform that we’re working with Azure and sets up some basic parameters.

Add the following code to your main.tf:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.22.0"
    }
  }
}

provider "azurerm" {
  features {}
  subscription_id = "your-subscription-id"  # Replace with your actual subscription ID
}

# We'll define our Azure resources here

Let’s break this down:

  • The terraform block specifies that we’re using the Azure Resource Manager (azurerm) provider.
  • The provider block configures the Azure provider with your subscription information.

For the subscription_id field, you’ll need to replace “your-subscription-id” with your actual Azure subscription ID. If you’re not sure what your subscription ID is, you can find it by running this command in your terminal:

1
az account list --output table

This will display a list of your Azure subscriptions. Copy the Subscription ID you want to use for this project.

Note: For more advanced scenarios, you might want to use environment variables instead of hardcoding your subscription ID. But for your first project, this approach keeps things simple and straightforward.

Initializing Our Terraform Working Directory

Now that we have our basic configuration set up, it’s time to initialize our Terraform working directory. We already initialized Terraform in the previous article, but we need to do it again here because we’re in a new directory with a new configuration.

  1. In your terminal, make sure you’re in your project directory.
  2. Run the following command:
1
terraform init

This command downloads the Azure provider plugin, sets up the directory for use with Terraform, and creates a .terraform directory to store the provider plugin and other necessary files.

Terraform initialization output showing successful download and installation of the Azure provider

If everything goes well, you should see a message saying “Terraform has been successfully initialized!” as shown in the screenshot above.

Congratulations! You’ve just set up and initialized your Terraform configuration for Azure. We’re ready to start defining our resources.

Defining Azure Resources in Terraform

Now that we’ve got our Terraform configuration initialized, it’s time to bring our Azure infrastructure to life. We’re going to define each of the resources we planned earlier in our main.tf file.

Resource Blocks in Terraform

In Terraform, we define Azure resources using resource blocks. The basic structure looks like this:

1
2
3
resource "type" "name" {
  # Resource configuration goes here
}
  • type is the type of Azure resource (like azurerm_resource_group)
  • name is a unique identifier for this resource in your Terraform config

Let’s define our resources one by one.

1. Resource Group

First, let’s create our resource group. Remember, this is like a container for all our other Azure resources. Add this to your main.tf:

1
2
3
4
5
# Resource Group
resource "azurerm_resource_group" "rg" {
  name     = "myTFResourceGroup"
  location = "Australia East"
}

Here, we’re creating a resource group named "myTFResourceGroup" in the Australia East region.

2. Virtual Network (VNet)

Next, let’s set up our virtual network. This will be our private network in Azure. Add this after your resource group definition:

1
2
3
4
5
6
7
# Virtual Network
resource "azurerm_virtual_network" "vnet" {
  name                = "myTFVNet"
  address_space       = ["10.0.0.0/16"]
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
}

Virtual Network definition in VSCode showing references to the resource group

Notice how we’re referencing the resource group we just created? The red arrows in the screenshot highlight these important connections:

  • We use azurerm_resource_group.rg.location to inherit the location from the resource group
  • We use azurerm_resource_group.rg.name to place our virtual network in the same resource group

This is one of Terraform’s most powerful features - resources can reference each other’s properties, creating relationships between your infrastructure components while keeping your code DRY (Don’t Repeat Yourself).

3. Subnet

Now, let’s carve out a subnet within our VNet:

1
2
3
4
5
6
7
# Subnet
resource "azurerm_subnet" "subnet" {
  name                 = "myTFSubnet"
  resource_group_name  = azurerm_resource_group.rg.name
  virtual_network_name = azurerm_virtual_network.vnet.name
  address_prefixes     = ["10.0.1.0/24"]
}

Subnet definition in VSCode showing dependencies on the resource group and virtual network

The red arrows in the screenshot highlight the key relationships:

  • We reference azurerm_resource_group.rg.name to place the subnet in the same resource group
  • We reference azurerm_virtual_network.vnet.name to create the subnet within our virtual network
  • We specify address_prefixes = ["10.0.1.0/24"] which carves out a portion of the larger 10.0.0.0/16 address space we defined for our VNet

These references create a hierarchy in our infrastructure: the resource group contains the virtual network, which contains the subnet. Terraform automatically understands these dependencies and will create resources in the correct order.

4. Network Security Group (NSG)

Time to add some security! Let’s create a network security group:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# Network Security Group
resource "azurerm_network_security_group" "nsg" {
  name                = "myTFNSG"
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name

  security_rule {
    name                       = "RDP"
    priority                   = 1001
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "3389"
    source_address_prefix      = "*"
    destination_address_prefix = "*"
  }
}

Network Security Group definition showing resource group references and security rule details

In this code:

  • The red arrows show how the NSG references the resource group for its location and name, keeping it in the same resource group as our other resources
  • The highlighted box shows our security rule configuration:
    • We’re allowing RDP traffic (port 3389)
    • The rule has a priority of 1001
    • It allows inbound traffic from any source to any destination

Important security note: In a production environment, you should never use "*" for source_address_prefix as shown here. This allows RDP connections from anywhere on the internet, which is a significant security risk. For real-world deployments, always restrict access to specific IP addresses or ranges. We’re using this open configuration only for demonstration purposes.

5. Network Security Group Association

Now that we’ve created our Network Security Group and subnet, we need to connect them. In Azure’s architecture, this connection requires a dedicated resource - it’s not just a simple property setting on either resource.

1
2
3
4
5
# Associate NSG with Subnet
resource "azurerm_subnet_network_security_group_association" "subnet_nsg_assoc" {
  subnet_id                 = azurerm_subnet.subnet.id
  network_security_group_id = azurerm_network_security_group.nsg.id
}

Network Security Group association with the subnet showing referenced resources

This might seem unusual if you’re new to Azure. Instead of simply adding an NSG property to a subnet, Azure requires a separate association resource that links them together. This highlights an important concept in Azure’s resource model:

  • Resources can exist independently (our subnet and NSG)
  • Their relationships are often managed as separate resources (the association)

Why does Azure work this way? This design offers more flexibility:

  • It allows one NSG to be easily associated with multiple subnets
  • It enables changing security rules without modifying the subnet itself
  • It provides clearer visibility into resource relationships

Important: Without this association, the security rules we defined in our NSG would exist but wouldn’t actually protect anything. The association is what activates our security rules on the subnet’s traffic.

This pattern of separate association resources appears in other parts of Azure as well, such as route table associations and service endpoint policies. Understanding this pattern will help you work with more complex Azure infrastructures in the future.

6. Network Interface Card (NIC)

Before we create our VM, we need to set up a network interface for it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Network Interface Card (NIC)
resource "azurerm_network_interface" "nic" {
  name                = "myTFNIC"
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name

  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.subnet.id
    private_ip_address_allocation = "Dynamic"
  }
}

Network Interface Card configuration showing resource group references and subnet association

The NIC connects our upcoming virtual machine to the network. In this configuration:

  • The red arrows show that, like our other resources, the NIC references the resource group for location and name
  • The highlighted box shows the IP configuration where:
    • We’re giving it a name of “internal”
    • We’re associating it with our previously created subnet (this places the VM in this subnet)
    • We’re setting the IP allocation to “Dynamic”, meaning Azure will automatically assign an IP address from the subnet range

This network interface serves as the bridge between our VM and the subnet. When we attach this NIC to our VM in the next step, the VM will effectively become part of our subnet and will be subject to the network security group rules we’ve configured.

If we needed a specific IP address instead of letting Azure choose one, we could change private_ip_address_allocation to “Static” and specify the exact IP address we want (as long as it’s within the subnet’s range).

7. Virtual Machine

Last but not least, let’s create our Windows virtual machine:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Windows Virtual Machine
resource "azurerm_windows_virtual_machine" "vm" {
  name                = "myTFVM"
  resource_group_name = azurerm_resource_group.rg.name
  location            = azurerm_resource_group.rg.location
  size                = "Standard_F2"
  admin_username      = "adminuser"
  admin_password      = "P@$$w0rd1234!"
  network_interface_ids = [
    azurerm_network_interface.nic.id,
  ]

  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }

  source_image_reference {
    publisher = "MicrosoftWindowsServer"
    offer     = "WindowsServer"
    sku       = "2019-Datacenter"
    version   = "latest"
  }
}

Virtual Machine configuration showing key settings

Our VM configuration ties everything together:

  • The red arrows point to familiar resource group references for location and name
  • The red box at the top highlights important VM settings:
    • We’re using “Standard_F2” size (2 vCPUs, 4 GB RAM)
    • We’re setting up admin credentials (username and password)
  • The arrow in the middle shows where we connect our previously created network interface card, which places the VM in our subnet
  • The red box at the bottom defines the OS image we’re using:
    • Windows Server 2019 Datacenter from Microsoft

This VM resource completes our infrastructure. Through the chain of references we’ve created (VM → NIC → Subnet → VNet → Resource Group), all our resources are now properly connected in a logical hierarchy.

Important security note: For this tutorial, we’re using a plaintext password in our configuration file. In a real-world scenario, you should never store passwords this way. Instead, use Azure Key Vault, environment variables, or Terraform variables marked as sensitive. Never commit credentials to version control!

For reference here is the complete configuration put together in one place:

 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.22.0"
    }
  }
}

provider "azurerm" {
  features {}
}

# Resource Group
resource "azurerm_resource_group" "rg" {
  name     = "myTFResourceGroup"
  location = "Australia East"
}

# Virtual Network
resource "azurerm_virtual_network" "vnet" {
  name                = "myTFVNet"
  address_space       = ["10.0.0.0/16"]
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
}

# Subnet
resource "azurerm_subnet" "subnet" {
  name                 = "myTFSubnet"
  resource_group_name  = azurerm_resource_group.rg.name
  virtual_network_name = azurerm_virtual_network.vnet.name
  address_prefixes     = ["10.0.1.0/24"]
}

# Network Security Group
resource "azurerm_network_security_group" "nsg" {
  name                = "myTFNSG"
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name

  security_rule {
    name                       = "RDP"
    priority                   = 1001
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "3389"
    source_address_prefix      = "*"
    destination_address_prefix = "*"
  }
}

# Associate NSG with Subnet
resource "azurerm_subnet_network_security_group_association" "subnet_nsg_assoc" {
  subnet_id                 = azurerm_subnet.subnet.id
  network_security_group_id = azurerm_network_security_group.nsg.id
}

# Network Interface Card (NIC)
resource "azurerm_network_interface" "nic" {
  name                = "myTFNIC"
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name

  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.subnet.id
    private_ip_address_allocation = "Dynamic"
  }
}

# Windows Virtual Machine
resource "azurerm_windows_virtual_machine" "vm" {
  name                = "myTFVM"
  resource_group_name = azurerm_resource_group.rg.name
  location            = azurerm_resource_group.rg.location
  size                = "Standard_F2"
  admin_username      = "adminuser"
  admin_password      = "P@$$w0rd1234!"
  network_interface_ids = [
    azurerm_network_interface.nic.id,
  ]

  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }

  source_image_reference {
    publisher = "MicrosoftWindowsServer"
    offer     = "WindowsServer"
    sku       = "2019-Datacenter"
    version   = "latest"
  }
}

With this final piece in place, we’ve defined all the resources for our Azure infrastructure. We’re now ready to plan and apply our configuration to create these resources in Azure.

Pro tip: If you want to quickly check your configuration for syntax errors without generating a full plan, you can run terraform validate. However, most errors will be caught during the plan stage anyway.

Planning Our Infrastructure Deployment

Before creating any resources, let’s preview what Terraform will do with the terraform plan command. This crucial step shows you exactly what will happen before any changes are made to your infrastructure.

What is Terraform Plan?

The terraform plan command is a crucial step in the Terraform workflow. It does a few important things:

  1. It checks your configuration against the current state of your infrastructure.
  2. It determines what changes need to be made to achieve the desired state described in your configuration.
  3. It presents a detailed plan of what actions Terraform will take when you apply your configuration.

Think of it as Terraform saying, “Here’s what I’m planning to do. Does this look right to you?”

Running and Understanding Terraform Plan

Let’s run the plan command to see what Terraform has in store:

1
terraform plan

The output shows exactly what Terraform will create when we apply our configuration:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
Terraform will perform the following actions:

  # azurerm_resource_group.rg will be created
  + resource "azurerm_resource_group" "rg" {
      + id       = (known after apply)
      + location = "australiaeast"
      + name     = "myTFResourceGroup"
    }

  # azurerm_virtual_network.vnet will be created
  + resource "azurerm_virtual_network" "vnet" {
      + address_space = [
          + "10.0.0.0/16",
        ]
      # ... more details ...
    }

  # ... (more resources)

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

In this output:

  • The + signs indicate resources that will be created
  • Some values show “(known after apply)” because they’re generated during creation
  • At the bottom, you can see a summary: 7 resources will be added

Take a moment to review this plan carefully. Check that:

  • All your expected resources are listed
  • Resource names and settings match what you intended
  • There are no unexpected changes or deletions

The plan is your chance to catch any mistakes before Terraform makes changes to your real infrastructure.

Pro tip: For team environments or critical infrastructure, you can save the plan with terraform plan -out=tfplan and then apply that specific plan later with terraform apply tfplan.

Now that we’ve previewed our changes and everything looks good, we’re ready to bring our infrastructure to life.

Applying Our Terraform Configuration

We’ve planned, we’ve prepared, and now it’s time for the main event - applying our Terraform configuration using the terraform apply command. This is where our code transforms into real, living infrastructure in Azure.

What is Terraform Apply?

The terraform apply command is where Terraform takes the plans we’ve made and turns them into reality. Here’s what happens when you run terraform apply:

  1. Terraform refreshes the state to make sure it has the most up-to-date information about your existing infrastructure.
  2. It creates a new plan (just like terraform plan did).
  3. It asks for your confirmation.
  4. If you confirm, it goes ahead and makes the necessary changes to your infrastructure.

Creating Our Resources

Run the following command in your terminal:

1
terraform apply

Terraform will show you the plan again (just like in the previous step) and ask for confirmation:

1
2
3
4
5
Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value:

Type yes and press Enter. Terraform will start creating your resources in the correct order, showing progress as it goes:

1
2
3
4
5
6
7
azurerm_resource_group.rg: Creating...
azurerm_resource_group.rg: Creation complete after 2s
azurerm_virtual_network.vnet: Creating...
azurerm_virtual_network.vnet: Creation complete after 5s
# ... more resources being created ...

Apply complete! Resources: 7 added, 0 changed, 0 destroyed.

Terraform follows the dependencies between resources, ensuring everything is created in the proper sequence. The resource group comes first, followed by the virtual network, and so on.

Once all resources are created, you’ll see a completion message:

1
Apply complete! Resources: 7 added, 0 changed, 0 destroyed.

What We’ve Accomplished

You’ve just created a complete Azure environment with a single command! Let’s appreciate what we’ve built:

  • A resource group as a container for all our resources
  • A virtual network with a subnet for network isolation
  • A network security group to protect our resources
  • A Windows virtual machine ready to run our applications

This demonstrates the power of Infrastructure as Code - we defined our entire environment in a few configuration files and deployed it consistently with a simple command.

If Something Goes Wrong

Don’t worry if you encounter errors during the apply process. Terraform provides detailed error messages to help you troubleshoot. Common issues include:

  • Azure permission problems
  • Resource name conflicts
  • Exceeding subscription limits

If you hit an error, read the message carefully, make the necessary adjustments to your configuration, and try again.

Congratulations! You’ve just deployed real infrastructure to Azure using Terraform. You’re now officially an infrastructure-as-code practitioner.

Now that our infrastructure is deployed, let’s verify that everything was created correctly.

Verifying Our Azure Resources

After creating our Azure resources with Terraform, let’s verify that everything was deployed correctly by checking the Azure Portal.

Exploring Resources in the Azure Portal

  1. Open your web browser and navigate to https://portal.azure.com
  2. Log in with your Azure credentials
  3. In the search bar at the top, type “Resource groups” and select it from the dropdown
  4. Look for the resource group we created (“myTFResourceGroup”)
  5. Click on the resource group to see all the resources inside it

You should see all the resources we created:

Azure Portal showing all resources created in myTFResourceGroup

As highlighted in the screenshot:

  • The resource group “myTFResourceGroup” contains all our resources
  • Inside it, you can see our network interface, network security group, virtual machine, OS disk, and virtual network

This confirms that Terraform successfully created all our resources according to our configuration.

Examining the Virtual Machine Details

If you click on the VM resource, you can verify its specific configuration:

Virtual Machine details showing configuration and network settings

The highlighted details in the red box confirm that:

  • The VM is part of myTFResourceGroup
  • It’s running Windows Server 2019 Datacenter
  • It has the Standard F2 size (2 vCPUs, 4 GB memory) as we specified
  • It’s located in Australia East
  • It’s connected to our virtual network (myTFVNet) and subnet (myTFSubnet)

Further down (see the red arrow), you can see the VM has been assigned a private IP address (10.0.1.4) from within our subnet range, just as we configured with the “Dynamic” allocation.

These screenshots confirm that our Terraform deployment was successful and all resources are configured exactly as defined in our Terraform files. This demonstrates the power of Infrastructure as Code – the ability to define your infrastructure in code and have it deployed consistently every time.

Important note about Terraform state: Terraform keeps track of all your resources in a file called terraform.tfstate. This file is crucial - it maps your configuration to real Azure resources. Never delete or manually edit this file, as it could prevent Terraform from properly managing your infrastructure. You can view the current state by running terraform show.

What If Something’s Not Right?

If you spot discrepancies between what you expected and what’s actually in Azure, don’t worry! Here’s what you can do:

  1. Double-check your Terraform configuration files for any mistakes
  2. Run terraform plan again to see if Terraform detects any differences
  3. If needed, make adjustments to your configuration and run terraform apply again

Remember, with infrastructure as code, it’s easy to make changes and keep your infrastructure in the desired state.

Now that we’ve confirmed our resources are created correctly, let’s explore how to make changes to our infrastructure.

Making Changes to Our Infrastructure

One of the most powerful aspects of Infrastructure as Code is how easily you can make changes to your existing infrastructure. Let’s see how Terraform handles modifications to our Azure environment.

The Scenario

Let’s say our application is becoming more popular, and we need to increase the computing power of our VM. In our original Terraform configuration, we defined our VM with a Standard_F2 size. We’ll change the VM size from Standard_F2 to Standard_F4, giving us more CPU and memory to handle the increased load.

Modifying Our Terraform Configuration

  1. Open your main.tf file.
  2. Find the virtual machine resource (azurerm_windows_virtual_machine.vm).
  3. Change the size attribute from "Standard_F2" to "Standard_F4":
1
2
3
4
5
6
7
8
resource "azurerm_windows_virtual_machine" "vm" {
  name                = "myTFVM"
  resource_group_name = azurerm_resource_group.rg.name
  location            = azurerm_resource_group.rg.location
  size                = "Standard_F4"  # Changed from Standard_F2 to Standard_F4
  
  # ... rest of the configuration remains the same
}

That’s it! This simple change tells Terraform that we want a bigger VM.

Planning the Changes

Before we apply this change, let’s see what Terraform plans to do:

  1. Save your changes to main.tf.
  2. In your terminal, run:
1
terraform plan

You should see output similar to this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Terraform will perform the following actions:

  # azurerm_windows_virtual_machine.vm will be updated in-place
  ~ resource "azurerm_windows_virtual_machine" "vm" {
        name                = "myTFVM"
      ~ size                = "Standard_F2" -> "Standard_F4"
        # (other unchanged attributes hidden)
    }

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

The ~ symbol indicates that Terraform will modify an existing resource rather than create a new one or destroy an old one.

Applying the Changes

If the plan looks good, let’s apply these changes:

1
terraform apply

Terraform will show you the plan again and ask for confirmation. Type yes and hit Enter.

You’ll see output showing Terraform updating the VM:

 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
28
29
azurerm_windows_virtual_machine.vm: Modifying... [id=/subscriptions/.../virtualMachines/myTFVM]
azurerm_windows_virtual_machine.vm: Still modifying... [id=/subscriptions/.../virtualMachines/myTFVM, 10s elapsed]
azurerm_windows_virtual_machine.vm: Modifications complete after 15s [id=/subscriptions/.../virtualMachines/myTFVM]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the
following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # azurerm_windows_virtual_machine.vm will be updated in-place
  ~ resource "azurerm_windows_virtual_machine" "vm" {
        id                                                     = "/subscriptions/7757257d-4cf8-44ee-a727-893dc886c3c4/resourceGroups/myTFResourceGroup/providers/Microsoft.Compute/virtualMachines/myTFVM"
        name                                                   = "myTFVM"
      ~ size                                                   = "Standard_F2" -> "Standard_F4"
        tags                                                   = {}
      ~ vm_agent_platform_updates_enabled                      = true -> false
        # (40 unchanged attributes hidden)

        # (2 unchanged blocks hidden)
    }

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

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value:

Enter yes to allow Terraform to apply the plan.

Understanding What Just Happened

Let’s break down what Terraform did:

  1. It compared the desired state (our updated configuration) with the current state (the existing infrastructure).
  2. It identified that only the VM size needed to change.
  3. It updated the VM in place, without affecting other aspects of the VM or other resources.

This is the beauty of Terraform - it makes only the necessary changes to bring your infrastructure in line with your configuration.

Verifying the Change

When Terraform finishes applying the change, it would show output similar to this:

1
2
3
4
azurerm_windows_virtual_machine.vm: Still modifying... [id=/subscriptions/7757257d-4cf8-44ee-a727-...crosoft.Compute/virtualMachines/myTFVM, 50s elapsed]
azurerm_windows_virtual_machine.vm: Modifications complete after 59s [id=/subscriptions/7757257d-4cf8-44ee-a727-893dc886c3c4/resourceGroups/myTFResourceGroup/providers/Microsoft.Compute/virtualMachines/myTFVM]

Apply complete! Resources: 0 added, 1 changed, 0 destroyed.

Looking at the VM in the Azure Portal, we can confirm the change was successful:

Azure Portal showing upgraded VM size of Standard F4 with 4 vCPUs and 8 GB memory

As highlighted in the screenshot, our VM has been upgraded from Standard F2 (2 vCPUs, 4 GB memory) to Standard F4 (4 vCPUs, 8 GB memory). This demonstrates how Terraform can efficiently modify existing resources without having to rebuild your entire infrastructure.

This is a key advantage of using Infrastructure as Code with Terraform – you can make incremental changes to your infrastructure by simply modifying your configuration files and applying the changes. Terraform handles the complex task of figuring out what needs to be changed and how to change it safely.

The Power of Infrastructure as Code

What we’ve just done demonstrates the real power of Infrastructure as Code:

  1. Predictability: We saw exactly what would change before applying it.
  2. Minimal Disruption: Only the necessary resource was modified.
  3. Version Control: If we’re using git, this change is now tracked in our repository.
  4. Repeatability: We can make this same change across multiple environments easily.

These benefits become even more valuable as your infrastructure grows more complex.

Now that we’ve successfully modified our infrastructure, let’s look at how to clean up our resources when we’re done.

Cleaning Up: Destroying Our Infrastructure

When you’re finished with your infrastructure, it’s important to clean up your resources to avoid unnecessary Azure charges. Terraform makes this process straightforward.

Why Clean Up is Important

Cleaning up unused resources is crucial for several reasons:

  1. Cost Management: Azure charges for resources even when they’re not actively used. Deleting unused resources saves money.
  2. Security: Reducing your infrastructure footprint minimizes potential security risks.
  3. Resource Limits: Azure subscriptions have limits on the number of resources you can create. Cleaning up frees up your quota.
  4. Organization: A tidy Azure subscription is easier to manage and understand.

The Terraform Destroy Command

Terraform makes it easy to remove all the resources we’ve created with a single command: terraform destroy. This command removes all the resources defined in your Terraform configuration.

Here’s how to use it:

  1. Make sure you’re in the directory containing your Terraform files.
  2. Run the following command:
1
terraform destroy

What to Expect

When you run terraform destroy, here’s what happens:

  1. Terraform plans the destruction of resources, similar to how terraform plan works.
  2. It shows you a summary of what will be destroyed.
  3. It asks for your confirmation before proceeding.

You’ll see output like this:

1
2
3
4
5
Do you really want to destroy all resources?
  Terraform will destroy all your managed infrastructure, as shown above.
  There is no undo. Only 'yes' will be accepted to confirm.

  Enter a value:

This is your last chance to review what’s about to be deleted. Double-check that you’re in the right directory and that you’re okay with destroying these resources.

If you’re sure, type yes and hit Enter.

Watching the Cleanup

Terraform will then proceed to destroy your resources. You’ll see output like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
azurerm_windows_virtual_machine.vm: Destroying... [id=/subscriptions/.../virtualMachines/myTFVM]
azurerm_windows_virtual_machine.vm: Still destroying... [id=/subscriptions/.../virtualMachines/myTFVM, 10s elapsed]
azurerm_windows_virtual_machine.vm: Destruction complete after 45s
azurerm_network_interface.nic: Destroying... [id=/subscriptions/.../networkInterfaces/myTFNIC]
azurerm_network_interface.nic: Destruction complete after 5s
...
azurerm_resource_group.rg: Destroying... [id=/subscriptions/.../resourceGroups/myTFResourceGroup]
azurerm_resource_group.rg: Still destroying... [id=/subscriptions/.../resourceGroups/myTFResourceGroup, 30s elapsed]
azurerm_resource_group.rg: Destruction complete after 45s

Destroy complete! Resources: 7 destroyed.

Terraform destroys resources in the reverse order of their dependencies, ensuring everything is cleaned up properly.

Verifying the Cleanup

After Terraform finishes, it’s a good idea to double-check that everything was removed:

  1. Log into the Azure portal
  2. Look for the resource group we created (“myTFResourceGroup”)
  3. It should no longer exist

You can also use Azure CLI to verify:

1
az group show --name myTFResourceGroup

This should return an error saying the resource group wasn’t found.

Best Practices for Cleanup

  1. Always Review the Plan: Before confirming the destroy operation, carefully review what Terraform plans to delete.
  2. Use Targeted Destroy: If you only want to remove specific resources, you can use terraform destroy -target=RESOURCE_TYPE.NAME for more granular control.
  3. Version Control: Keep your Terraform configurations in version control. Even after destroying resources, you’ll have a record of what was created.
  4. Backup State: Before destroying, consider backing up your Terraform state file. This can be helpful for auditing or if you need to recreate the infrastructure later.

A Note on Terraform State: After destroying your resources, your Terraform state file (terraform.tfstate) will be updated to reflect that no resources are currently managed. If you run terraform show now, you should see no resources listed.

Conclusion

Congratulations! You’ve just completed your first end-to-end Terraform project on Azure. Let’s take a moment to appreciate how far you’ve come:

  1. You’ve set up a Terraform configuration for Azure resources
  2. Planned and applied your infrastructure changes
  3. Modified existing resources using Terraform
  4. Responsibly cleaned up your resources

You’ve experienced the full lifecycle of infrastructure as code. That’s a significant achievement!

Key Takeaways

  • Infrastructure as Code makes managing cloud resources more efficient and repeatable
  • Terraform allows you to define, create, modify, and destroy Azure resources using code
  • Always plan before you apply to avoid surprises
  • Cleaning up unused resources is crucial for cost management and security

What’s Next?

As you’ve been working through this tutorial, you might have noticed Terraform referring to something called “state”. What is this mysterious state, and why is it so important?

In our next article, “Understanding Terraform State Management in Azure”, we’ll dive deep into this crucial concept. You’ll learn:

  • What Terraform state is and why it’s essential
  • How Terraform uses state to track your resources
  • Best practices for managing state in Azure
  • How to work with remote state for team collaboration

State management is a key part of working with Terraform, especially in team environments or when dealing with larger, more complex infrastructures. It’s the mechanism that allows Terraform to know what it’s managing and how to make changes efficiently.

Remember, every expert was once a beginner. Don’t be afraid to experiment, make mistakes, and learn from them. That’s the beauty of infrastructure-as-code – you can always tear it down and start again!

Keep practicing, keep learning, and most importantly, have fun building amazing things with Terraform and Azure. The cloud’s the limit! ☁️🚀

Stay strong 💪, and happy Terraforming!