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:
- In our first article, we explored the fundamentals of Terraform and why it’s a game-changer for managing Azure infrastructure.
- 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:
- Resource Group - A container that keeps all our Azure resources neatly organized
- Virtual Network (VNet) - Our own private network in Azure that provides the networking foundation
- Subnet - A range of IP addresses within our VNet that helps organize network traffic
- Network Security Group (NSG) - Our virtual firewall that controls what traffic can flow in and out
- 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:
|
|
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:
- Open your terminal or command prompt.
- Navigate to where you want to create your project.
- Create a new directory and move into it:
|
|
You’ve just created a home for your Terraform project.
Creating Our Main Configuration File
Now, let’s create our main Terraform configuration file:
- In your project directory, create a new file named
main.tf
. - 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
:
|
|
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:
|
|
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.
- In your terminal, make sure you’re in your project directory.
- Run the following command:
|
|
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.
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:
|
|
type
is the type of Azure resource (likeazurerm_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
:
|
|
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:
|
|
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:
|
|
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:
|
|
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
"*"
forsource_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.
|
|
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:
|
|
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:
|
|
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:
|
|
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:
- It checks your configuration against the current state of your infrastructure.
- It determines what changes need to be made to achieve the desired state described in your configuration.
- 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:
|
|
The output shows exactly what Terraform will create when we apply our configuration:
|
|
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 withterraform 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
:
- Terraform refreshes the state to make sure it has the most up-to-date information about your existing infrastructure.
- It creates a new plan (just like
terraform plan
did). - It asks for your confirmation.
- If you confirm, it goes ahead and makes the necessary changes to your infrastructure.
Creating Our Resources
Run the following command in your terminal:
|
|
Terraform will show you the plan again (just like in the previous step) and ask for confirmation:
|
|
Type yes
and press Enter. Terraform will start creating your resources in the correct order, showing progress as it goes:
|
|
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:
|
|
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
- Open your web browser and navigate to https://portal.azure.com
- Log in with your Azure credentials
- In the search bar at the top, type “Resource groups” and select it from the dropdown
- Look for the resource group we created (“myTFResourceGroup”)
- Click on the resource group to see all the resources inside it
You should see all the resources we created:
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:
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 runningterraform 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:
- Double-check your Terraform configuration files for any mistakes
- Run
terraform plan
again to see if Terraform detects any differences - 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
- Open your
main.tf
file. - Find the virtual machine resource (
azurerm_windows_virtual_machine.vm
). - Change the
size
attribute from"Standard_F2"
to"Standard_F4"
:
|
|
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:
- Save your changes to
main.tf
. - In your terminal, run:
|
|
You should see output similar to this:
|
|
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:
|
|
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:
|
|
Enter yes
to allow Terraform to apply the plan.
Understanding What Just Happened
Let’s break down what Terraform did:
- It compared the desired state (our updated configuration) with the current state (the existing infrastructure).
- It identified that only the VM size needed to change.
- 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:
|
|
Looking at the VM in the Azure Portal, we can confirm the change was successful:
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:
- Predictability: We saw exactly what would change before applying it.
- Minimal Disruption: Only the necessary resource was modified.
- Version Control: If we’re using git, this change is now tracked in our repository.
- 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:
- Cost Management: Azure charges for resources even when they’re not actively used. Deleting unused resources saves money.
- Security: Reducing your infrastructure footprint minimizes potential security risks.
- Resource Limits: Azure subscriptions have limits on the number of resources you can create. Cleaning up frees up your quota.
- 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:
- Make sure you’re in the directory containing your Terraform files.
- Run the following command:
|
|
What to Expect
When you run terraform destroy
, here’s what happens:
- Terraform plans the destruction of resources, similar to how
terraform plan
works. - It shows you a summary of what will be destroyed.
- It asks for your confirmation before proceeding.
You’ll see output like this:
|
|
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:
|
|
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:
- Log into the Azure portal
- Look for the resource group we created (“myTFResourceGroup”)
- It should no longer exist
You can also use Azure CLI to verify:
|
|
This should return an error saying the resource group wasn’t found.
Best Practices for Cleanup
- Always Review the Plan: Before confirming the destroy operation, carefully review what Terraform plans to delete.
- Use Targeted Destroy: If you only want to remove specific resources, you can use
terraform destroy -target=RESOURCE_TYPE.NAME
for more granular control. - Version Control: Keep your Terraform configurations in version control. Even after destroying resources, you’ll have a record of what was created.
- 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 runterraform 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:
- You’ve set up a Terraform configuration for Azure resources
- Planned and applied your infrastructure changes
- Modified existing resources using Terraform
- 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!