Structuring Azure Infrastructure with Terraform — A Modular, Environment-Aware Approach
Infrastructure teams often face the same dilemma: start simple and refactor later, or invest in structure upfront and move slower at first…
Structuring Azure Infrastructure with Terraform — A Modular, Environment-Aware Approach
Infrastructure teams often face the same dilemma: start simple and refactor later, or invest in structure upfront and move slower at first. After managing Azure resources through a series of brittle, monolithic Terraform configurations, we chose the latter — and it paid off.
In this post, we walk through the directory layout, provider configuration, remote state setup, and module design we use to manage our Data platform across development and production environments on Azure. The patterns are straightforward, but the reasoning behind each choice matters.
Why structure matters in Terraform
Terraform makes it easy to get started, and equally easy to end up with a 2,000-line main.tf that no one dares touch. Shared state, environment-specific drift, and tangled provider configs are the natural enemies of scale.
Our solution is a layered folder structure where environments are isolated, shared logic lives in modules, and variables enforce discipline at every boundary.

The directory layout
Everything lives under a top-level terraform/ directory. Each environment gets its own folder; reusable logic goes into shared modules.
terraform/ ├── shared/ │ └── modules/ │ ├── dev/ │ ├── main.tf │ ├── variables.tf │ └── dev.tfvars │ ├── prod/ │ ├── main.tf │ ├── variables.tf │ └── prod.tfvars
Key Design Principles
- Reusability → Common logic is placed in
shared/modules - Environment isolation → Each environment has its own configuration
- Scalability → Easy to extend for new regions or teams
- Governance → Standardized tagging and naming conventions
Terraform Module Setup
To ensure reusability and consistency, we organize our infrastructure using modular Terraform design.
All reusable components are placed under the shared/modules directory, allowing multiple teams and environments to use the same standardized logic.
Each module is designed to represent a specific resource or service (e.g., Resource Group, Storage, Key Vault), making the infrastructure easy to scale and maintain.
Modelule Structure

Module Structure
Resource Group Module
resource "azurerm_resource_group" "resource_group" {
name = var.resource_group_name
location = var.location
tags = var.tags
}
Configuring providers and remote state
The main.tf in each environment handles three responsibilities: pinning provider versions, configuring the Azure backend, and wiring up module calls. Here is the dev configuration:
terraform {
required_providers = {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.42"
}
databricks = {
source = "databricks/databricks"
version = "~> 1.0"
}
azuread = {
source = "hashicorp/azuread"
version = "~> 2.0"
}
}
backend "azurerm" {
resource_group_name = var.resource_group_name
storage_account_name = var.storage_account_name
container_name = var.container_name
key = var.file_path
}
}
provider "azurerm" {
features {}
}
provider "azuread" {
tenant_id = var.tenant_id
}
Notice that the backend uses variables rather than hardcoded values. This is intentional — it lets each environment point to its own Azure Storage container without changing the code, only the *.tfvars file.
Why pin provider versions? Terraform providers release frequently, and minor bumps can introduce breaking changes. Using version constraints like
~> 3.42allows patch updates while guarding against unexpected major changes in shared pipelines.
Calling shared modules — the security resource group
With providers and backend in place, the real work begins: calling modules. Here is how we provision the security resource group in the dev environment, applying a rich set of governance tags through Terraform’s merge() function:
dev/main.tf — security resource group moduleHCL
module "resource_group_security" {
source = "./shared/modules/resource_group"
resource_group_name = "rg-dp-security-${var.environment}-weu-01"
location = var.location
tags = merge(var.tags, {
ApplicationName = "DataPlatform-AI"
BusinessUnit = "DataPlatform"
CostCenter = "DL-XX-001"
WorkType = "INFRA"
ServiceCategory = "Security"
BillingOwner = "DataEngineering"
Confidentiality = "Internal"
Criticality = "High"
CreatedBy = "Terraform"
ExpirationDate = "None"
Description = "Security resources: NSG, Private Endpoints, Routing"
})
}
The naming convention rg-dp-security-${var.environment}-weu-01 dynamically resolves to rg-dp-security-dev-weu-01 in development and rg-dp-security-prod-weu-01 in production — with identical module code.
Why Tagging Matters
In a multi-team and multi-environment setup, tagging is not just metadata — it is critical for:
- Cost tracking and chargeback
- Resource ownership identification
- Security and compliance classification
- Operational governance
Terraform Exuection Commond
1terraform init — wire up providers and backend
Downloads providers, initialises the remote backend, and resolves module sources. Run this once per environment, and again whenever providers or modules change.
2 terraform plan — inspect changes before they happen
Compares your configuration against real Azure state and produces a diff. Nothing changes in Azure — this is read-only. Always save the plan output for apply.
3terraform apply — execute with explicit approval Applies the
메타데이터
- post_id
- c8de80c0ec35
- slug
- structuring-azure-infrastructure-with-terraform-a-modular-environment-aware-approach-c8de80c0ec35
- url
- https://medium.com/@jagdishmwagh/structuring-azure-infrastructure-with-terraform-a-modular-environment-aware-approach-c8de80c0ec35
- canonical_url
- https://medium.com/@jagdishmwagh/structuring-azure-infrastructure-with-terraform-a-modular-environment-aware-approach-c8de80c0ec35
- author_url
- https://medium.com/@jagdishmwagh
- status
- ok
- fetched_at
- 2026-09-05 03:25:13