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 (.tf files).
  • 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 (and destroy to 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 .tfvars files 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.

Keep reading: Configure Azure SQL serverless · Design & Architecture.

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.