Terraform and Infrastructure as Code (IaC): fundamentals, project structure and connecting to Azure
Building infrastructure “by hand” in the Azure portal doesn’t scale: it’s hard to repeat, audit and version. Infrastructure as Code (IaC) solves that, and Terraform is today one of its most popular tools. Here you’ll see the fundamentals, the basic structure of a new project, how to connect it to Azure and how it differs from ARM.
What is Infrastructure as Code (IaC)?
IaC means managing your infrastructure (networks, servers, databases, storage) through versioned code files instead of manual clicks. Benefits:
- Repeatable: the same code creates the same environment again and again (identical dev, QA, prod).
- Versioned: it lives in Git — history, reviews and rollback.
- Auditable: changes are reviewed in pull requests before being applied.
- Idempotent: applying twice doesn’t duplicate anything; it converges to the desired state.
Terraform fundamentals
Terraform (by HashiCorp) is declarative: you describe the desired state and it figures out how to get there. Key concepts:
- HCL: the configuration language (
.tffiles). - Providers: plugins that talk to each platform (azurerm for Azure, aws, google, etc.). It’s multi-cloud.
- State: Terraform keeps a state file mapping your code to the real resources. It’s central — it must live in a shared remote backend with locking.
- Workflow:
init → plan → apply(anddestroyto remove).
terraform init # downloads providers and configures the backend
terraform plan # PREVIEWS the changes (without applying)
terraform apply # applies the changes
terraform destroy # removes what was created
The plan is one of its best qualities: you see exactly what will be created, changed or destroyed before touching anything.
Basic structure of a new project
A tidy Terraform project separates concerns across files. A recommended structure:
my-project/
├── providers.tf # providers and required versions
├── backend.tf # remote state configuration
├── main.tf # the resources
├── variables.tf # input variables
├── outputs.tf # output values (IPs, names, etc.)
├── terraform.tfvars # concrete values (NOT secrets)
└── modules/ # reusable modules
└── network/
├── main.tf
├── variables.tf
└── outputs.tf
- providers.tf / backend.tf: the “skeleton” — which providers you use and where the state lives.
- main / variables / outputs: the standard pattern of every module.
- modules/: package reusable pieces (a network, a database) and reuse them across environments.
- Environments (dev/prod): separate them with different
.tfvarsfiles or folders/workspaces, reusing the same modules.
Connecting Terraform to Azure: important parameters
For Azure you use the azurerm provider. Three things to keep in mind:
1. The provider block (with features)
terraform {
required_version = ">= 1.5"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
}
}
provider "azurerm" {
features {} # required, even if empty
}
Pin the provider version (version) for reproducible builds.
2. Authentication
The recommended approach is a Service Principal, passing credentials via environment variables (never in the code):
export ARM_SUBSCRIPTION_ID="your-subscription-id"
export ARM_TENANT_ID="your-tenant-id"
export ARM_CLIENT_ID="service-principal-app-id"
export ARM_CLIENT_SECRET="service-principal-secret"
For local development you can also authenticate with az login (Azure CLI). In pipelines, a Service Principal or Managed Identity is ideal.
3. Remote state (azurerm backend)
Store the state in an Azure Storage blob — that way the team shares the same state and Terraform applies locking (preventing concurrent applies):
terraform {
backend "azurerm" {
resource_group_name = "rg-tfstate"
storage_account_name = "sttfstate001"
container_name = "tfstate"
key = "prod.terraform.tfstate"
}
}
And an example resource:
resource "azurerm_resource_group" "main" {
name = var.rg_name
location = var.location
}
Terraform vs ARM (and Bicep)
ARM templates are Azure’s native language (JSON). Bicep is ARM’s modern evolution: a cleaner DSL that compiles to ARM, also Azure-native. Terraform is third-party (HashiCorp), multi-cloud and with its own state.
| Terraform | ARM / Bicep | |
|---|---|---|
| Language | HCL | JSON (ARM) / Bicep |
| Scope | Multi-cloud (Azure, AWS, GCP…) | Azure only (native) |
| State | Its own state file (you manage it) | No state — Azure tracks it |
| Preview | Very clear plan |
what-if (more limited) |
| New Azure features | Often arrive with some delay | Day-zero support (native) |
| Ecosystem | Huge (modules, providers) | Azure-focused |
When to use each: Terraform if you work multi-cloud, already have experience with it, or want its ecosystem and preview. ARM/Bicep if you’re 100% Azure, want native integration, immediate support for new features and don’t want to manage a state file.
Conclusion
IaC turns your infrastructure into something repeatable, versioned and auditable; Terraform does it multi-cloud and with a very clear preview. Start with a tidy structure (providers, backend, variables, modules), authenticate with a Service Principal and store the state remotely from day one.
At Grupo TANDEM we design and implement Azure infrastructure with IaC, ready to scale and audit. If you want to set up your Terraform foundation right from the start, let’s talk.
Need help with this?
At Grupo TANDEM we implement it for you. Let's talk about your case.
Talk to an advisor