Blue-green deployment is a release strategy that lets you ship new versions of your app with near-zero downtime and low risk. Instead of updating your live app directly, you run two environments side-by-side and switch traffic between them.
In this guide, Iβll walk you through how I implemented blue-green deployment on Azure using Terraform and simple HTML apps. This is written for beginners and focuses on understanding why we do each step β not just what to type.
π§ What Is Blue-Green Deployment (Simple Explanation)
Imagine:
Blue = current live version
Green = new version
Users only see one version at a time.
You:
Deploy the new version to Green
Test it safely
Swap Green β Production
Instantly roll back if needed
No downtime. No risky in-place updates.
Azure App Service deployment slots make this easy.
π― What We Will Build
We will:
β Create Azure infrastructure with Terraform β Create a staging slot β Deploy two app versions (Blue & Green) β Swap them using Terraform β Understand how real companies do this
π Prerequisites
You should have:
Azure subscription
Terraform (by HashiCorp) installed
Azure CLI installed
Logged in using az login
Basic Terraform knowledge
ποΈ Step 1 β Create Resource Group, App Service Plan & App Service
Why these resources?
Resource Group Container that holds everything.
App Service Plan Defines pricing tier, performance, and features. Deployment slots require Standard tier or higher.
Terraform + Azure Entra ID Mini Project: Step-by-Step Beginner Guide (Users & Groups from CSV)
In this mini project, I automated user and group management in Microsoft Entra ID using Terraform.
Instead of creating infrastructure like VMs or VNets, we manage:
π€ Users
π₯ Groups
π Group memberships
I followed my instructorβs tutorial but implemented it in my own small, testable steps. This blog shows exactly how you can do the same and debug easily as a beginner.
π― What Weβre Building
We will:
β Fetch our tenant domain β Read users from a CSV file β Create Entra ID users from CSV β Detect duplicate usernames β Create a group β Add users to the group based on department
resource "azuread_group_member" "education" {
for_each = {
for u in azuread_user.users :
u.mail_nickname => u
if u.department == "Education"
}
group_object_id = azuread_group.test_group.id
member_object_id = each.value.id
}
Apply
terraform apply
Verify
Portal β Groups β Members tab
β Only Education department users added.
π§ Key Beginner Lessons
β Work in Small Steps
Donβt deploy everything at once.
β Always Check Data First
Validate CSV before creating resources.
β Use Outputs for Debugging
Outputs save hours of troubleshooting.
β Terraform is Declarative
It maintains the desired state automatically.
π What You Can Try Next
π Add more users to CSV π Create groups by job title π Use Service Principal authentication π Generate random passwords π Assign roles to groups
π Final Thoughts
This project shows how powerful Terraform is beyond infrastructure β it can manage identity too.
If you’re learning cloud or DevOps, this skill is extremely valuable because real organizations manage thousands of users and groups.
Start small, test often, and build confidence step-by-step β exactly like you did here.
In this mini project, I implemented Azure VNet peering using Terraform, but instead of applying everything at once, I deliberately broke the setup into small, testable steps. This approach makes it much easier to understand whatβs happening, catch mistakes early, and build real confidence with Terraform and Azure networking.
Below is the exact flow I followed β and you can follow the same steps as a beginner.
This mini project demonstrates how to build a real-world Azure infrastructure step by step using Terraform. The goal is not just to deploy resources, but to understand why each Azure service exists, how it fits into the architecture, and what each Terraform block actually does.
Instead of creating everything in one go, we intentionally build the infrastructure incrementally. This makes it easier for beginners to:
Verify resources in the Azure Portal
Understand dependencies between services
Debug errors without feeling overwhelmed
Build a strong mental model of Azure networking and compute
What We Are Building (End Architecture Overview)
By the end of this project, we will have:
A Resource Group to logically contain all resources
A Virtual Network (VNet) with a defined private IP space
A Subnet to host compute resources
A Network Security Group (NSG) acting as a firewall
A Public IP for inbound internet access
A Standard Load Balancer to distribute traffic
A NAT Gateway to manage outbound internet traffic
A Virtual Machine Scale Set (VMSS) running a web application
This architecture closely resembles how production web applications are deployed on Azure.
Step 1: Resource Group, Virtual Network, and Subnet
Why this step is required
In Azure, nothing can exist without a Resource Group. Similarly, no virtual machine can exist outside a Virtual Network.
This step lays the networking foundation for everything that follows.
Terraform Built-in Functions (Part 1): Learning Functions Through Hands-on Assignments
In this section, we begin exploring Terraform built-in functions through practical, hands-on assignments. Instead of only reading documentation, the focus here is on:
Practicing functions directly in terraform console
Applying them in real Terraform files
Solving common problems such as:
Formatting names
Enforcing naming rules
Merging maps
Validating resource constraints
Generating dynamic values
This approach helps beginners understand why functions exist and how to use them correctly.
Practicing Functions Using terraform console
Before writing full Terraform files, we can experiment with functions interactively.
terraform console
Inside the console, you can directly test functions.
Example:
max(2, 4, 1)
Result:
4
This shows:
You do not need to write a full Terraform file
You can quickly test function behavior
This is the fastest way to learn functions safely
Terraform only supports built-in functions. You cannot create custom functions in Terraform.
Assignment 1: Formatting Resource Names with lower and replace
Requirement:
Resource names must:
Be lowercase
Replace spaces with hyphens
Input example:
Project Alpha Resource
Expected output:
project-alpha-resource
Step 1: Define the Variable
variable "project_name" {
type = string
description = "Name of the project"
default = "Project Alpha Resource"
}
Step 3: Build a Map of NSG Rules Using a for Expression
locals {
nsg_rules = {
for port in local.formatted_ports :
"Port-${port}" => {
name = "Port-${port}"
port = port
description = "Allow traffic on port ${port}"
}
}
}
Explanation:
for port in local.formatted_ports Loops through each port
In this first part of Terraform functions, you learned how to:
Practice functions using terraform console
Format names using:
lower
replace
substr
Merge maps using:
merge
Enforce provider naming rules using nested functions
Convert strings to lists using:
split
Generate multiple blocks using:
for expressions
Dynamic maps
These assignments show how Terraform functions help you write:
Cleaner code
Fewer hardcoded values
More reusable configurations
Provider-compliant resource definitions
This forms the foundation for writing dynamic, production-ready Terraform code.
Terraform Built-in Functions (Part 2): Practical Demos with Lookup, Validation, Sets, Math, Time, and Files
In this section, we continue learning Terraform built-in functions through a set of hands-on assignments. The focus here is on how functions are used in real Terraform code to solve practical problems such as:
Selecting values dynamically
Validating user input
Enforcing naming rules
Removing duplicates
Performing math on lists
Working with timestamps
Handling sensitive data and files
All examples below are written in a beginner-friendly, step-by-step way.
Using lookup to Select Values from an Environment Map
Instead of writing long conditional expressions, we use a map + lookup function to select the correct VM size based on the environment.
Defining the Environment Variable with Validation
variable "environment" {
type = string
description = "Environment name"
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Enter a valid value for environment: dev, staging, or prod"
}
}
Explanation:
contains(["dev", "staging", "prod"], var.environment) Ensures the value is only one of the allowed environments
If the value is invalid, Terraform stops with the custom error message
This prevents accidental typos like prods or testing.
Mapping Environments to VM Sizes
variable "vm_sizes" {
type = map(string)
default = {
dev = "Standard_D2s_v3"
staging = "Standard_D4s_v3"
prod = "Standard_D8s_v3"
}
}
This map defines which VM size should be used in each environment.
output "credential" {
value = var.credential
sensitive = true
}
Terraform will display:
credential = <sensitive>
This prevents secrets from being printed in logs.
Enforcing Naming Rules with endswith
We ensure backup names end with _backup.
variable "backup_name" {
type = string
default = "test_backup"
validation {
condition = endswith(var.backup_name, "_backup")
error_message = "Backup name must end with _backup"
}
}
If the name does not end with _backup, Terraform stops with an error.
Combining Lists and Removing Duplicates with concat and toset
Terraform File and Directory Structure Best Practices
As your Terraform projects grow, keeping everything in a single file becomes messy and hard to maintain. In this section, weβll learn how to structure Terraform files properly and how Terraform decides the order in which resources are created using dependencies.
This will help you write clean, scalable, and error-free Terraform code.
Splitting Terraform Code into Multiple Files
Terraform allows you to split your configuration into multiple .tf files.
β You can move each block (provider, resources, variables, outputs, etc.) into different files β Terraform automatically loads all .tf files in a directory β File names can be anything meaningful
Example of a Clean File Structure
You might organize your project like this:
main.tf β main resources
providers.tf β provider configuration
variables.tf β input variables
outputs.tf β output variables
locals.tf β local variables
backend.tf β backend configuration
β οΈ Important: File names donβt control execution order β dependencies do.
Some Blocks Must Be Inside Parent Blocks
Certain Terraform configurations must be nested inside parent blocks, such as the backend.
depends_on = [ azurerm_resource_group.example ] Forces Terraform to create the resource group first Even if Terraform wouldnβt detect the dependency automatically
β οΈ Use explicit dependency only when necessary β implicit is preferred.
Best Practices Summary
To keep your Terraform projects clean and reliable:
β Split code into meaningful files β Donβt rely on file name order for execution β Always use resource references to create implicit dependencies β Use depends_on only when required β Keep backend configuration inside the terraform block β Organize directories logically as projects grow
Terraform Type Constraints Explained (Through an Azure VM Example)
In this section, weβll understand Terraform Type Constraints by actually creating an Azure Virtual Machine step by step. Instead of theory alone, weβll see how each data type is used in real Terraform code.
Weβll cover:
Primitive types: string, number, bool
Collection types: list, map, set
Structural types: tuple, object
Starting Point: Azure VM Terraform Documentation
To understand which fields expect which types, we first look at the official Azure VM resource documentation:
# azurerm_network_interface.main will be created
# azurerm_resource_group.example will be created
# azurerm_subnet.internal will be created
# azurerm_virtual_machine.main will be created
# azurerm_virtual_network.main will be created
This confirms Terraform is reading your types correctly.
List Type (Collection Type)
A list holds multiple values of the same type, in a fixed order.
var.vm_config.publisher Accesses the publisher field from the object
Same pattern for offer, sku, and version
This keeps VM image configuration clean and centralized.
Summary
In this section, you learned how Terraform type constraints work by using:
string β resource names and prefixes
number β disk size
bool β delete OS disk flag
list(string) β multiple locations
map(string) β tags
tuple(...) β mixed network configuration
set(string) β unique VM sizes
object({...}) β structured VM configuration
Understanding these types is essential to avoid type mismatch errors and to write robust, reusable Terraform code.
Terraform Resource Meta-Arguments: count and for_each
In this section, weβll learn about Terraform Resource Meta-Arguments, specifically:
count
for_each
These meta-arguments allow you to create multiple resources in a loop using collections like lists, sets, and maps.
Weβll use a practical example: creating multiple Azure Storage Accounts, and weβll also see how to output the names of created resources, which is a very common real-world requirement.
Why Meta-Arguments Are Needed
Without count or for_each, you would have to:
Write one resource block per storage account
Duplicate the same code again and again
With meta-arguments, you can:
Write the resource once
Dynamically create many instances
Control creation using variables
This makes your Terraform code:
Cleaner
More scalable
Easier to maintain
Using count to Create Multiple Resources
count is best suited when:
You are working with a list
The order of items matters
You want to access elements using an index
Defining a List of Storage Account Names
variable "storage_account_names" {
type = list(string)
description = "storage account names for creation"
default = ["myteststorageacc222j22", "myteststorageacc444l44"]
}
Line-by-line Explanation
type = list(string) Declares a list where every element must be a string
default = [ ... ] Defines two storage account names in a fixed order
No changes occur, but the resource is now protected.
Step 3: Try to Destroy the Resource
Now attempt to destroy the infrastructure:
terraform destroy
Terraform will fail with an error similar to:
Error: Instance cannot be destroyed
Resource azurerm_storage_account.example has lifecycle.prevent_destroy set,
but the plan calls for this resource to be destroyed.
What This Shows
Terraform is telling you:
This resource is marked as non-destructible
The operation is blocked
Nothing will be deleted
This proves that prevent_destroy is working.
Step 4: How to Intentionally Destroy a Protected Resource
To destroy a resource with prevent_destroy, you must explicitly remove the protection first.
Remove the lifecycle block:
lifecycle {
prevent_destroy = true
}
Run:
terraform apply
Then run:
terraform destroy
Only now will Terraform allow the resource to be deleted.
This ensures:
Deletion is always a conscious, intentional action
Important Rules About prevent_destroy
It blocks:
terraform destroy
Replacements that require destroy
Deletions caused by config changes
It does not block:
In-place updates
Reading the resource
Drift detection
It applies only to Terraform actions
It does not prevent manual deletion in the Azure Portal
When Not to Use prevent_destroy
Avoid using it when:
The resource is temporary
You use frequent tear-down environments (dev, test)
You rely on automated cleanup pipelines
Overusing prevent_destroy can:
Block automation
Cause stuck pipelines
Require manual intervention
Use it only for truly critical resources.
Summary
In this section, you learned:
What prevent_destroy does
Why it is essential for protecting critical infrastructure
How Terraform behaves without it
How to demo it by:
Adding prevent_destroy
Running terraform destroy
Observing the blocked operation
How to safely remove the protection when deletion is required
This lifecycle rule is Terraformβs strongest safety mechanism for preventing catastrophic accidental deletions in production environments.
Terraform Lifecycle replace_triggered_by: What It Is and How to Demo It
In this section, weβll learn about the Terraform lifecycle rule replace_triggered_by:
What it does
Why it exists
When you should use it
How to demo it clearly using Azure
This rule is used when you want Terraform to force replacement of a resource when some other resource or attribute changes.
What Is replace_triggered_by?
By default, Terraform replaces a resource only when:
One of its own attributes changes
And that change requires replacement
The lifecycle rule:
lifecycle {
replace_triggered_by = [ ... ]
}
Tells Terraform:
βIf this other resource or attribute changes, then recreate this resource as well, even if this resource itself did not change.β
In simple words:
You define a trigger
When the trigger changes
Terraform forces replacement of this resource
Why replace_triggered_by Is Important
You should use replace_triggered_by when:
One resource is tightly coupled to another
An in-place update is not safe
You want to guarantee a fresh recreation
Common real-world examples:
Recreate a VM when its image version changes
Recreate an app when a config file changes
Recreate a resource when a subnet changes
Recreate a resource when a secret or key changes
In short:
It gives you explicit control over replacement behavior.
How to Demo replace_triggered_by
We will demo this using:
One Azure Resource Group
One Azure Storage Account
One simple trigger resource
We will:
Create the resources
Link them using replace_triggered_by
Change only the trigger
Observe that Terraform replaces the storage account
Now add a custom condition to the storage account.
resource "azurerm_storage_account" "example" {
name = "democonditionacc01"
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
account_tier = "Standard"
account_replication_type = "LRS"
lifecycle {
precondition {
condition = startswith(self.name, "demo")
error_message = "Storage account name must start with 'demo'."
}
}
}
Line-by-line Explanation
lifecycle { ... } Declares lifecycle rules for this resource
precondition { ... } Defines a validation rule that runs before creation or update
condition = startswith(self.name, "demo") Checks that the storage account name begins with "demo"
error_message = "..." Message shown if the condition fails
Apply again:
terraform apply
No change occurs, because the condition is satisfied.
Step 3: Break the Condition Intentionally
Now change the name to an invalid value:
name = "invalidacc01"
Run:
terraform plan
You will see an error like:
Error: Resource precondition failed
Storage account name must start with 'demo'.
What This Shows
This proves that:
Terraform evaluated the condition
The condition returned false
Terraform stopped before creating or modifying anything
This is the core power of custom conditions.
Demo Using postcondition
Now letβs see a simple postcondition.
We will check that the storage account location is really "West Europe".
resource "azurerm_storage_account" "example" {
name = "democonditionacc01"
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
account_tier = "Standard"
account_replication_type = "LRS"
lifecycle {
postcondition {
condition = self.location == "West Europe"
error_message = "Storage account was not created in West Europe."
}
}
}
What This Does
Terraform creates or reads the resource
Then checks the condition
If the actual location is not "West Europe", Terraform fails
This validates the real result, not just the input.
Where Else Can You Use Custom Conditions?
You can use custom conditions in:
resource blocks
data blocks
output blocks
Example on output:
output "storage_account_name" {
value = azurerm_storage_account.example.name
precondition {
condition = length(self) > 3
error_message = "Storage account name is too short."
}
}
This validates outputs before showing them.
Important Rules About Custom Conditions
They fail the plan or apply immediately
They do not fix problems, only detect them
They improve safety, not automation
Overuse can make configs too strict
They should contain clear error messages
When Not to Use Custom Conditions
Avoid using them when:
The rule is already enforced by the provider
The rule is too flexible to express in code
You want to allow experimentation in dev
Use them mainly for:
Production guardrails
Organizational policies
Hard technical requirements
Summary
In this section, you learned:
What custom conditions are
The difference between precondition and postcondition
Why they are important for safe Terraform code
How to demo them by:
Adding a precondition
Breaking the rule intentionally
Observing Terraform fail with a custom error
How to validate real infrastructure using postcondition
Custom conditions turn Terraform from a simple provisioning tool into a rule-enforcing, self-validating infrastructure platform.
Terraform Dynamic Expressions: Why We Need Dynamic Blocks and How They Work with Azure NSG
In this section, weβll understand why Terraform dynamic blocks are needed, how NSG rules look without dynamic blocks, and why in this demo we store rule values in locals and use them inside a dynamic block instead of looping through a simple list.
This explanation is based on your exact Azure Network Security Group demo code.
Official documentation for Azure NSG using terraform:
Why This Design Is Better Than Without Dynamic Blocks
With locals + dynamic blocks:
Resource code stays constant
Rules are data-driven
Easy to extend and modify
Ideal for modules and production use
Clean separation of:
Configuration data
Resource logic
Without dynamic blocks:
Code grows quickly
Hard to maintain
High chance of mistakes
Poor scalability
Summary
In this section, you learned:
How NSG rules look without dynamic blocks
Why hardcoding repeated security_rule blocks does not scale
Why dynamic blocks are needed for repeated nested blocks
Why storing rules in locals as a map is better than:
Hardcoding
Using simple lists
How security_rule.key and security_rule.value work
How Terraform converts data into real configuration
This pattern β maps in locals + dynamic blocks in resources β is a key step from basic Terraform to clean, scalable, production-grade Infrastructure as Code.
Terraform Conditional Expressions: Dynamically Naming an NSG Based on Environment
In this section, weβll learn how to use a Terraform conditional expression to dynamically set the name of an Azure Network Security Group (NSG) based on the value of an environment variable.
This is a practical beginner example that shows how:
One Terraform codebase
Can create different resource names
For different environments like dev and staging
Without changing the code itself
Weβll explain this using the exact code and CLI output from your demo.
The Problem We Are Solving
In real projects, you rarely deploy only one environment.
You usually have:
Development (dev)
Staging (staging)
Testing (test)
Production (prod)
Each environment must have:
Different resource names
To avoid conflicts
To keep environments isolated
Without conditional logic, you would need:
Separate Terraform files per environment, or
Manual edits before every deployment
Terraform conditional expressions solve this cleanly.
This is a simple but very powerful example of how Terraform conditional expressions make your infrastructure flexible, automated, and production-ready.
Terraform Splat Expression: Collecting Values from Multiple Resources
In this section, weβll learn about the Terraform splat expression and how it is used to collect values from multiple instances of a resource into a single list.
Weβll cover:
What a splat expression is
Why splat expressions are needed
When you typically use them
The syntax of splat expressions
A simple demo with multiple resources
How this is commonly used with count and for_each
Splat expressions are a key concept when you start working with multiple resource instances in Terraform.
What Is a Splat Expression?
A splat expression is a shortcut syntax used to:
Extract the same attribute From all instances of a resource And return them as a list.
Basic syntax:
resource_type.resource_name[*].attribute
Example:
azurerm_storage_account.example[*].name
This means:
Take all instances of azurerm_storage_account.example
Get the name attribute from each one
Return a list of names
Why We Need Splat Expressions
Splat expressions are useful when:
You create multiple resources using:
count
for_each
You want to:
Output all names
Pass all IDs to another resource
Collect all IP addresses
Build a list from many instances
Without splat:
You would have to reference each instance manually:
Why splat expressions are needed to collect values
How splat works with:
count
for_each
How to use splat in output variables
The difference between:
Legacy *. syntax
Modern [*] syntax
Splat expressions are one of the most important tools for working with multiple resource instances and building data flows between Terraform resources.
Terraform Built-in Functions: Useful String, List & Map Helpers
Terraform comes with a set of built-in functions you can use inside expressions to transform values, manipulate strings, work with lists or maps, and more. These functions are extremely helpful when you want to process values dynamically in a module, variable, local, or resource attribute.
Terraform Fundamentals: Infrastructure as Code (IaC) for Beginners
In this section, weβll cover the core fundamentals of Terraform and Infrastructure as Code (IaC). If you are completely new to Terraform or cloud automation, this will give you a strong conceptual foundation before we move into hands-on labs in later sections.
What Is Infrastructure as Code (IaC)?
Infrastructure as Code means provisioning and managing infrastructure using code instead of manual steps.
Traditionally, an engineer would log in to a cloud portal (Azure, AWS, or GCP), click through screens, fill forms, and create resources like virtual machines, load balancers, and databases. With IaC, you write code that defines what infrastructure you want, and a tool provisions it for you automatically.
In simple terms:
You write code
The code creates infrastructure
Your infrastructure becomes repeatable, predictable, and automated
This is exactly what tools like Terraform help us achieve.
Popular Infrastructure as Code Tools
There are multiple IaC tools available today:
Terraform β Cloud-agnostic (works with Azure, AWS, GCP, and more)
Pulumi β Uses programming languages like Python, TypeScript
Azure ARM Templates / Bicep β Azure-native IaC tools
AWS CloudFormation β AWS-native IaC tool
GCP Deployment Manager β GCP-native IaC tool
In this series, the focus is Terraform, because it works across multiple cloud providers and is widely used in the industry.
Why Do We Need Infrastructure as Code?
You might wonder:
βIf I can easily create resources using the cloud portal, why should I write code?β
Letβs understand this with a simple example.
Example: A Basic Three-Tier Architecture
Imagine a simple application setup:
Web Tier β Handles user requests
App Tier β Processes business logic
Database Tier β Stores data
In a real cloud environment, this includes:
Multiple virtual machines
Auto-scaling or VM scale sets
External and internal load balancers
Health probes and networking
Highly available databases (primary + replica)
Creating this manually using a cloud portal might take 2 hours for just one environment.
The Real Enterprise Challenge
In real organizations, you donβt have just one environment.
Typically, you have multiple environments like:
Development (Dev)
User Acceptance Testing (UAT)
System Integration Testing (SIT)
Pre-Production
Production
Disaster Recovery (DR)
Thatβs 6 environments.
If one environment takes 2 hours, manually creating all of them takes 12+ hours β and thatβs just for provisioning.
Problems With Manual Infrastructure Provisioning
Manual infrastructure creation introduces several serious challenges:
Time-Consuming
Engineers spend hours just setting up infrastructure instead of developing or testing applications.
High Cost
Resources often remain running even when not in use, increasing cloud bills.
Repetitive Work
The same steps are repeated daily across environments.
Human Errors
Manual clicks and form entries can easily lead to misconfigurations.
Security Issues
Inconsistent role-based access and permissions can expose security risks.
βIt Works on My Machineβ Problem
Each environment may differ slightly:
Different software versions
Missing patches
Configuration mismatches
This leads to bugs appearing in production even when code works in development.
How Terraform Solves These Problems
Terraform addresses all these challenges effectively.
Automation
Automatically create, update, and destroy infrastructure
No manual clicks or repetitive tasks
Consistency
Same code creates identical environments
Eliminates configuration drift
Cost Optimization
Easily destroy infrastructure when not needed
Avoid paying for unused resources
Faster Delivery
Provision complex infrastructure in minutes
Focus more on development and testing
Version Control
Terraform code is stored in Git
Full history, reviews, and approvals via pull requests
Reusability
Write once, deploy many times
Use variables to create multiple environments from the same code
How Terraform Works (High-Level)
Terraform follows a simple workflow:
Terraform Configuration Files
Written using .tf files
Stored in a Git repository
Key Terraform Commands
terraform init Initializes Terraform and downloads required provider plugins
terraform validate Checks syntax and basic configuration errors
terraform plan Shows a preview of changes (dry run)
terraform apply Applies changes and creates/updates infrastructure
terraform destroy Removes infrastructure when itβs no longer needed
These commands can be run manually or automated using CI/CD pipelines.
Installing Terraform
Terraform installation is straightforward and supports all major operating systems.
Installation Options
macOS
Windows
Linux
FreeBSD and others
Summary
By the end of this section, you should understand:
What Infrastructure as Code is
Why manual infrastructure doesnβt scale
How Terraform solves real-world enterprise problems
The basic Terraform workflow
How to install Terraform on your system
Terraform Providers Explained (Beginner-Friendly)
In this section, weβll focus entirely on Terraform Providers. Providers are a core building block of Terraform, and understanding them early will make everything else much easier as we move forward.
What Is a Terraform Provider?
A Terraform provider is a plugin that allows Terraform to talk to external systems such as:
Cloud platforms (Azure, AWS, GCP)
Container platforms (Kubernetes, Docker)
Monitoring and tooling platforms (Datadog, Prometheus)
Terraform itself does not know how to create a virtual machine, database, or load balancer. Instead, it relies on providers to translate Terraform code into API calls that cloud platforms understand.
Terraform Version vs Provider Version
This is a very important concept for beginners.
Terraform CLI version This is the version of Terraform installed on your machine (for example, 1.9.x).
Terraform provider version Each provider (Azure, AWS, etc.) has its own version, maintained independently.
π These two versions are separate and serve different purposes.
Types of Terraform Providers
Terraform providers are categorized based on who maintains them:
When you run Terraform commands, this is what happens:
You write Terraform configuration files (.tf)
Terraform reads the configuration
The provider plugin:
Translates your configuration
Calls the target platformβs API (Azure, AWS, etc.)
The platform creates or updates resources
Terraform displays the result
π Providers act as a bridge between Terraform and real infrastructure.
Why Providers Are Required
Each platform has:
Different APIs
Different authentication methods
Different resource definitions
Providers standardize this complexity so you can write consistent Terraform code without worrying about API differences.
Thatβs why Terraform supports hundreds of providers.
Provider Configuration Basics
A Terraform configuration usually includes two key parts:
Terraform Block
This defines:
Required Terraform version
Required providers and their versions
Provider Block
This configures how Terraform connects to the platform (credentials, features, regions, etc.)
Example (conceptual, not full code):
Specify the provider source
Lock a provider version
Configure provider-specific settings
Why You Must Lock Provider Versions
If you donβt specify a provider version, Terraform uses the latest version by default.
This can cause problems because:
New versions may remove or rename fields
Existing Terraform code may break unexpectedly
Best Practice
Use the provider version you developed and tested with
Upgrade versions intentionally, not automatically
This process is called version locking.
Understanding Provider Version Constraints
Terraform allows you to control which versions are allowed using version operators.
Here are the most common ones:
Exact Version (=)
Uses only one specific version
No upgrades allowed
Exclude Version (!=)
Uses any version except the excluded one
Greater Than / Less Than (>=, <=)
Uses versions based on comparison rules
Pessimistic Constraint (~>) β Most Important
This allows safe updates.
Example behavior:
Allows patch updates (bug fixes)
Prevents breaking changes
If your version is:
3.0.2 β allowed: 3.0.5
Not allowed: 3.1.0
This keeps your infrastructure stable while still receiving fixes.
When Should You Upgrade a Provider?
Follow this workflow:
Upgrade the provider locally
Test all Terraform changes
Validate nothing breaks
Promote changes to higher environments
Never upgrade providers directly in production.
Key Takeaways
By the end of this section, you should understand:
What Terraform providers are
Why providers are required
Difference between Terraform and provider versions
Types of providers
How providers interact with APIs
Why version locking is critical
How version constraints work
Install Terraform in Windows Using PowerShell and Chocolatey(Windows package manager)
Using Chocolatey, a Windows package manager, is one of the easiest ways to install Terraform. It handles downloading, installation, and updates automatically.
Step 1: Install Chocolatey
Chocolatey must be installed first before we can use it to install Terraform.
This command temporarily bypasses the execution policy and installs Chocolatey securely.
Note: Close and reopen PowerShell after installation to ensure Chocolatey is available.
Step 2: Install Terraform
Once Chocolatey is installed, run the command below to install Terraform:
choco install terraform -y
The -y flag automatically confirms prompts and installs the latest stable version.
Step 3: Verify the Installation
To confirm that Terraform has been installed successfully, run:
terraform -version
If Terraform displays its version number, the installation is complete β
Summary
Using Chocolatey simplifies the installation process by:
Eliminating manual downloads
Automatically configuring the system PATH
Making future updates easy with a single command
Youβre now ready to start working with Terraform π
First Terraform Code: Create an Azure Storage Account (Beginner Friendly)
In this section, weβll write and run our first Terraform Infrastructure as Code (IaC) to create an Azure Storage Account. This is written for absolute beginnersβno prior Terraform knowledge assumed.
We will:
Discover the correct Terraform resource
Write Terraform code step by step
Authenticate Terraform with Azure
Understand what each Terraform command does
Create and clean up Azure resources safely
Finding the Right Terraform Resource
When working with Terraform, the official documentation is your best friend.
Search in Google:
Create Azure Storage Account using Terraform
Youβll land on the Terraform Registry page for the Azure Storage Account resource.
Environment variables are not referenced in .tf files
The Azure provider automatically reads them
This keeps secrets out of code
When Terraform runs, the provider internally does something like: βIf subscription_id is not set in the provider block, check ARM_SUBSCRIPTION_ID.β
Initializing Terraform
terraform init
1. Downloads the Azure Provider
Terraform reads the required_providers block and downloads the AzureRM provider specified in your configuration.
This provider is responsible for:
Communicating with Azure APIs
Creating, updating, and deleting Azure resources
2. Creates the .terraform Directory
Terraform creates a hidden directory named:
.terraform/
This directory contains:
Provider binaries (for Windows, these are .exe files)
Metadata Terraform needs to interact with the provider
These binaries are platform-specific, which is why this folder should never be committed to GitHub.
3. Generates terraform.lock.hcl
Terraform creates a file called:
terraform.lock.hcl
β This file locks provider versions
What this file actually does:
Records the exact provider versions used
Ensures everyone running the project uses the same provider versions
Prevents unexpected changes due to provider upgrades
This guarantees consistent and repeatable builds across machines and environments.
Validating Configuration
terraform validate
This checks:
Syntax correctness
Required arguments
Logical errors
No resources are created at this stage.
How Terraform Reads .tf Files
When Terraform runs:
It loads all .tf files in the directory
Treats them as one single configuration
File order does not matter
Planning the Infrastructure Changes
terraform plan
This command:
Compares desired state vs actual state
Shows what Terraform will do
Makes no changes
Example output summary:
Plan: 2 to add, 0 to change, 0 to destroy.
Filtering Plan Output (PowerShell)
To see only created resources:
terraform plan | Select-String "will be created"
This is the PowerShell equivalent of grep.
Applying the Configuration
terraform apply
Terraform:
Shows the plan again
Asks for confirmation
Creates the resources in Azure
Making Changes Later
Terraform follows desired state:
Modify .tf files
Run terraform plan
Apply changes
Some changes require resource recreation, which Terraform clearly shows.
Cleaning Up Resources (Recommended)
Instead of manually deleting resources:
terraform destroy
To skip confirmation:
terraform destroy --auto-approve
This ensures:
Clean teardown
No orphaned resources
No unexpected cloud costs
Key Takeaways
Terraform code defines desired state
Providers read credentials automatically from environment variables
plan shows changes, apply executes them
All .tf files in a folder act as one configuration
destroy is the safest cleanup method
This completes your first real Terraform IaC workflow π
Terraform State File Management with Azure Storage (Beginner Friendly)
In this section, we will understand what the Terraform state file is, why it is critical, and how to manage it safely using Azure Storage as a remote backend.
This is a core concept in Terraform, and understanding it early will save you from many production issues later.
What Is the Terraform State File?
When you run Terraform for the first time, it automatically generates a file called:
terraform.tfstate
This file contains the actual current state of your infrastructure.
Terraform uses this file to:
Know what resources already exist
Compare current state with desired state
Decide what changes to make
Why Terraform Needs a State File
In your Terraform code (.tf files), you only describe the desired state, for example:
βI want one resource group and one storage account.β
But Terraform also needs to know:
What already exists
What was created earlier
What needs to change
This information comes entirely from the state file.
Without the state file:
Terraform cannot track resources
It may try to recreate everything
It may lose control of your infrastructure
Important Rules About the State File
The Terraform state file is extremely sensitive. Follow these rules strictly:
Never modify it manually
Always back it up
Protect it like a secret
Do not share it publicly
Best Practices for Managing Terraform State
Do Not Store State Locally
By default, Terraform stores terraform.tfstate in your local folder. This is fine for learning, but not for real projects.
Instead, store it in a remote backend, such as:
Azure Blob Storage
AWS S3
Google Cloud Storage
This allows:
Team collaboration
Centralized access
Better security
Reliable recovery
Use State Locking
State locking ensures:
Only one Terraform process can modify infrastructure at a time.
This prevents:
Two engineers applying changes simultaneously
Corrupted state files
Race conditions
Azure Storage supports native state locking, which Terraform uses automatically.
Isolate State Files for Each Environment
Never use one state file for all environments.
Best practice:
One state file per environment
Example:
dev.tfstate
qa.tfstate
prod.tfstate
This ensures:
Changes in dev do not affect production
Clean separation of infrastructure
Keep Regular Backups
Even with remote backends:
Accidental deletion
Corruption
Misconfiguration can happen
Always ensure your backend supports:
Versioning
Soft delete
Backups
Azure Blob Storage supports all of these.
Why We Use Azure Storage for Terraform State
Azure Blob Storage is commonly used because:
It is secure
Highly available
Supports state locking
Supports versioning and backups
Integrates well with Terraform
Importantly:
This storage account is NOT managed by Terraform It is created and maintained separately.
This avoids circular dependency issues.
Creating the Remote Backend Infrastructure
We do NOT use Terraform to create the backend storage.
Instead, we use:
Azure Portal
or Azure CLI
This ensures Terraform can safely access the backend from the start.
What we create:
A resource group
A storage account
A blob container
This container will hold the Terraform state file remotely.
Configuring Terraform to Use Azure Backend
Google search: Azure backend terraform, to find a page about how to configure remote backend in terraform
backend "azurerm" Tells Terraform to use Azure Storage as backend
resource_group_name The resource group where the storage account exists
storage_account_name The Azure storage account holding the state
container_name Blob container where the state file will be stored
key The name of the state file inside the container (This allows multiple environments to coexist)
How Terraform Authenticates to Azure Storage
Terraform uses:
Azure CLI authentication OR
Environment variables (ARM_CLIENT_ID, etc.)
The same authentication you use for Azure resources is reused for backend access.
What Happens After Backend Is Configured
After adding the backend block, run:
terraform init
Terraform will:
Connect to Azure Storage
Create the state file remotely
Migrate local state (if any)
Enable locking automatically
You will now see:
A minimal local state file
The real state stored securely in Azure Blob Storage
Why Remote State Is Critical
Without remote state:
Teams cannot collaborate safely
Infrastructure can drift
State can be lost or corrupted
CI/CD pipelines cannot function properly
With remote state:
Terraform becomes production-ready
Safe team operations are possible
Infrastructure remains predictable
Key Takeaways
Terraform state file tracks actual infrastructure
Never modify or expose it manually
Always use a remote backend for real projects
Azure Blob Storage is a reliable backend choice
State locking and isolation are mandatory for safety
Terraform Variables Explained
In this section, weβll learn about Terraform Variables, one of the most important concepts for writing reusable and flexible Infrastructure as Code. Variables help you avoid hardcoding values and make your Terraform configurations adaptable to different environments like dev, staging, and production.
Types of Variables in Terraform
Terraform supports three main kinds of variables:
Input Variables
These allow you to pass values into Terraform from outside (CLI, files, etc.).
Output Variables
These allow Terraform to return values after execution, such as resource names, IP addresses, or IDs.
Local Variables
These are used inside Terraform code only for convenience and readability.
Type Constraints in Terraform
Terraform supports several data types:
Primitive Types
string β text values
number β numeric values
bool β true or false
These take only one value.
Collection Types
list β ordered collection of values
set β unordered unique values
map β key-value pairs
Structural Types
object β complex structure with named attributes
tuple β fixed sequence of values
Special Type
any β use when you donβt know the type in advance (not recommended for beginners unless necessary)
Input Variables
Input variables allow your Terraform code to be dynamic and reusable.
Output variables let Terraform display useful values after execution.
Defining an Output Variable
output "storage_account_name" {
value = azurerm_storage_account.example.name
}
Line-by-line Explanation
output "storage_account_name" Declares an output variable named storage_account_name
value = azurerm_storage_account.example.name Extracts the name of the storage account created in Terraform (azurerm_storage_account.example refers to a resource defined elsewhere)
Why Output Variables Are Useful
They help you:
See created resource details
Pass values between Terraform modules
Use outputs in scripts or pipelines
You can view outputs using:
terraform output
Or also after:
terraform plan
terraform refresh
Local Variables
Local variables are used for internal convenience when values do not change frequently.