Skip to content

Terraform on Azure -- Complete Guide to Provisioning Azure Infrastructure

DodaTech 6 min read

Terraform on Azure enables you to define and provision the full Microsoft Azure infrastructure stack -- virtual networks, compute, databases, Kubernetes, and storage -- using declarative HCL configuration files version-controlled in Git.

What You'll Learn

In this tutorial, you will learn how to configure the Azure provider, provision virtual networks with subnets, deploy VMs with managed disks, create Azure SQL databases, set up AKS clusters, manage storage accounts with policies, and implement Azure RBAC with Terraform.

Why It Matters

Azure's portal-driven management does not scale for teams managing dozens of subscriptions and hundreds of resources. Terraform provides a unified declarative workflow across all Azure services, with plan preview, dependency management, and CI/CD integration.

Real-World Use

DodaTech uses Terraform to manage Azure infrastructure for enterprise clients alongside AWS deployments. Durga Antivirus Pro's threat intelligence pipeline runs on Azure AKS clusters provisioned entirely through Terraform, with Azure SQL for threat data storage and Blob Storage for malware samples.

Azure Provider Configuration

graph TD
    A[Terraform Azure Config] --> B[AzureRM Provider]
    B --> C[Virtual Network]
    B --> D[Azure VM]
    B --> E[Azure SQL]
    B --> F[AKS]
    B --> G[Storage Account]
    B --> H[Azure AD / RBAC]
    C --> I[Subnets]
    C --> J[NSG]
    C --> K[VPN Gateway]
    D --> L[Managed Disks]
    D --> M[Availability Sets]
    F --> N[Node Pools]
    F --> O[ACR Integration]
    style A fill:#0078d4,color:#fff
    style B fill:#0078d4,color:#fff

Provider Setup with Azure AD Authentication

# provider.tf
terraform {
  required_version = ">= 1.6"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.0"
    }
  }
}

provider "azurerm" {
  features {
    resource_group {
      prevent_deletion_if_contains_resources = true
    }
    virtual_machine {
      delete_os_disk_on_deletion     = true
      skip_shutdown_and_force_delete = false
    }
  }

  subscription_id = var.subscription_id
  use_oidc        = true
}
# Login via Azure CLI (or OIDC in CI)
az login
terraform init

Expected output:

Initializing the backend...
Initializing provider plugins...
- Finding hashicorp/azurerm versions matching "~> 4.0"...
- Installing hashicorp/azurerm v4.3.0...
- Installed hashicorp/azurerm v4.3.0 (signed by HashiCorp)

Resource Group and Virtual Network

# networking.tf
resource "azurerm_resource_group" "main" {
  name     = "rg-${var.environment}-${var.location}"
  location = var.location

  tags = {
    Environment = var.environment
    ManagedBy   = "Terraform"
  }
}

resource "azurerm_virtual_network" "main" {
  name                = "vnet-${var.environment}"
  location            = azurerm_resource_group.main.location
  resource_group_name = azurerm_resource_group.main.name
  address_space       = [var.vnet_cidr]

  tags = {
    Environment = var.environment
  }
}

resource "azurerm_subnet" "public" {
  name                 = "snet-${var.environment}-public"
  resource_group_name  = azurerm_resource_group.main.name
  virtual_network_name = azurerm_virtual_network.main.name
  address_prefixes     = [var.public_subnet_cidr]
}

resource "azurerm_subnet" "private" {
  name                 = "snet-${var.environment}-private"
  resource_group_name  = azurerm_resource_group.main.name
  virtual_network_name = azurerm_virtual_network.main.name
  address_prefixes     = [var.private_subnet_cidr]
}

resource "azurerm_network_security_group" "web" {
  name                = "nsg-${var.environment}-web"
  location            = azurerm_resource_group.main.location
  resource_group_name = azurerm_resource_group.main.name

  security_rule {
    name                       = "HTTP"
    priority                   = 100
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "80"
    source_address_prefixes    = ["*"]
    destination_address_prefix = "*"
  }

  security_rule {
    name                       = "HTTPS"
    priority                   = 101
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "443"
    source_address_prefixes    = ["*"]
    destination_address_prefix = "*"
  }
}

Azure Kubernetes Service (AKS)

# aks.tf
resource "azurerm_kubernetes_cluster" "main" {
  name                = "aks-${var.environment}"
  location            = azurerm_resource_group.main.location
  resource_group_name = azurerm_resource_group.main.name
  dns_prefix          = "dodatech-${var.environment}"
  kubernetes_version  = "1.30"

  default_node_pool {
    name                = "system"
    node_count          = 2
    vm_size             = "Standard_D4s_v5"
    enable_auto_scaling = true
    min_count           = 2
    max_count           = 5
    vnet_subnet_id      = azurerm_subnet.private.id
  }

  identity {
    type = "SystemAssigned"
  }

  network_profile {
    network_plugin = "azure"
    network_policy = "calico"
    service_cidr   = "10.96.0.0/12"
    dns_service_ip = "10.96.0.10"
  }

  azure_active_directory_role_based_access_control {
    managed            = true
    azure_rbac_enabled = true
    tenant_id          = data.azurerm_client_config.current.tenant_id
  }

  oidc_issuer_enabled       = true
  workload_identity_enabled = true

  tags = {
    Environment = var.environment
  }
}
terraform plan -out=aks.tfplan
terraform apply aks.tfplan

Expected output:

Apply complete! Resources: 4 added, 0 changed, 0 destroyed.

Outputs:
aks_cluster_name = "aks-production"
aks_kube_config = <sensitive>

Azure SQL Database

# database.tf
resource "azurerm_mssql_server" "main" {
  name                         = "sql-${var.environment}-${var.location}"
  resource_group_name          = azurerm_resource_group.main.name
  location                     = azurerm_resource_group.main.location
  version                      = "12.0"
  administrator_login          = var.sql_admin_username
  administrator_login_password = random_password.sql_admin.result

  azuread_administrator {
    login_username = data.azurerm_client_config.current.display_name
    object_id      = data.azurerm_client_config.current.object_id
  }

  tags = {
    Environment = var.environment
  }
}

resource "azurerm_mssql_database" "main" {
  name                = "dodatech-${var.environment}"
  server_id           = azurerm_mssql_server.main.id
  collation           = "SQL_Latin1_General_CP1_CI_AS"
  license_type        = "LicenseIncluded"
  max_size_gb         = var.environment == "production" ? 500 : 50
  read_scale          = var.environment == "production" ? true : false
  sku_name            = var.environment == "production" ? "GP_Gen5_8" : "GP_Gen5_2"
  zone_redundant      = var.environment == "production" ? true : false

  tags = {
    Environment = var.environment
  }
}

Storage Account with Data Protection

# storage.tf
resource "azurerm_storage_account" "data" {
  name                     = "stdodatech${var.environment}"
  resource_group_name      = azurerm_resource_group.main.name
  location                 = azurerm_resource_group.main.location
  account_tier             = "Standard"
  account_replication_type = var.environment == "production" ? "GRS" : "LRS"
  account_kind             = "StorageV2"

  blob_properties {
    versioning_enabled = true

    delete_retention_policy {
      days = 30
    }

    container_delete_retention_policy {
      days = 30
    }
  }

  network_rules {
    default_action = "Deny"
    bypass         = ["AzureServices"]
    ip_rules       = var.allowed_admin_ips
  }

  tags = {
    Environment = var.environment
  }
}

resource "azurerm_storage_container" "malware_samples" {
  name                  = "malware-samples"
  storage_account_name  = azurerm_storage_account.data.name
  container_access_type = "private"
}

Azure RBAC with Terraform

# rbac.tf
data "azurerm_client_config" "current" {}

resource "azurerm_role_assignment" "aks_contributor" {
  scope                = azurerm_kubernetes_cluster.main.id
  role_definition_name = "Azure Kubernetes Service Cluster User Role"
  principal_id         = data.azurerm_client_config.current.object_id
}

resource "azurerm_role_assignment" "acr_pull" {
  scope                = azurerm_container_registry.main.id
  role_definition_name = "AcrPull"
  principal_id         = azurerm_kubernetes_cluster.main.kubelet_identity.0.object_id
}

Common Mistakes

1. Missing features Block

The features block in provider "azurerm" is required. Without it, Terraform cannot manage certain Azure resource behaviors.

2. Not Using OIDC Authentication

Using Azure service principal secrets in CI exposes credentials. Use OIDC with workload identity federation instead.

3. Forgetting Network Security Rules

Azure VNets are isolated by default. Missing NSG rules cause connectivity failures between services.

4. SQL Server Firewall Rules

Azure SQL blocks all traffic by default. Configure firewall_rule resources to allow application access.

5. AKS Node Pool Sizing

Undersized node pools cause scheduling failures in production. Use auto-scaling with minimum and maximum node counts.

Practice Questions

1. How do you authenticate the AzureRM provider with OIDC? Set use_oidc = true in the provider block and configure workload identity federation in Azure AD.

2. What does the features block control in the AzureRM provider? It configures resource behavior like VM disk deletion, resource group protection, and key vault soft-delete.

3. How do you enable Azure AD integration for AKS? Set azure_active_directory_role_based_access_control with managed = true and azure_rbac_enabled = true in the AKS cluster resource.

4. Challenge: Write a Terraform configuration that provisions an AKS cluster with Azure AD integration, a private ACR, and workload identity for pod-to-Azure-service authentication.

Mini Project: Azure Three-Tier Architecture

Provision a virtual network with public and private subnets, an application gateway in the public subnet, an AKS cluster in the private subnet, Azure SQL Database with private endpoint, and a storage account with blob versioning. Use RBAC for least-privilege access.

Terraform on AWS
Terraform on GCP

What's Next

Deploy Azure infrastructure with Terraform for your workloads, then explore GCP with Terraform for multi-cloud deployments. Study Cloud Computing strategies for hybrid cloud architectures.

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro