Why Terraform for Azure?
Bicep is Azure's native IaC language, but Terraform has become the multi-cloud standard for many teams. Its main advantage: one tool to manage AWS, Azure, GCP, and third-party providers (Datadog, Cloudflare, GitHub). The azurerm provider, maintained by HashiCorp and Microsoft, is comprehensive and very active — over 700 supported resources.
This guide covers deploying a production-ready AKS cluster with Terraform: networking, identities, node pools, ACR, and a CI/CD pipeline.
Prerequisites: Service Principal and Backend
# Create a Service Principal for Terraform
az ad sp create-for-rbac --name "sp-terraform-aks" --role Contributor --scopes /subscriptions/${SUBSCRIPTION_ID} --json-auth
# Create Storage Account for Terraform backend
az group create --name rg-terraform-state --location francecentral
az storage account create --name stterraformstate001 --resource-group rg-terraform-state --sku Standard_LRS --allow-blob-public-access false
az storage container create --name tfstate --account-name stterraformstate001
Terraform Project Structure
infra/
├── main.tf # Providers + backend
├── variables.tf # Input variables
├── outputs.tf # Outputs (kubeconfig, etc.)
├── network.tf # VNet, subnets, NSG
├── aks.tf # AKS cluster + node pools
├── acr.tf # Azure Container Registry
├── identity.tf # Managed Identity + role assignments
└── environments/
├── dev.tfvars
└── prd.tfvars
AKS Cluster Resource
resource "azurerm_kubernetes_cluster" "main" {
name = "aks-${var.environment}"
resource_group_name = azurerm_resource_group.main.name
location = var.location
dns_prefix = "aks-${var.environment}"
kubernetes_version = var.kubernetes_version
sku_tier = "Standard" # 99.95% SLA
workload_identity_enabled = true
oidc_issuer_enabled = true
default_node_pool {
name = "system"
vm_size = "Standard_D4s_v5"
node_count = 3
zones = ["1", "2", "3"]
vnet_subnet_id = azurerm_subnet.nodes.id
pod_subnet_id = azurerm_subnet.pods.id
only_critical_addons_enabled = true
}
network_profile {
network_plugin = "azure"
network_plugin_mode = "overlay"
load_balancer_sku = "standard"
}
auto_upgrade_profile {
upgrade_channel = "patch"
}
azure_active_directory_role_based_access_control {
managed = true
azure_rbac_enabled = true
}
}
Spot Node Pool
resource "azurerm_kubernetes_cluster_node_pool" "spot" {
name = "spot"
kubernetes_cluster_id = azurerm_kubernetes_cluster.main.id
vm_size = "Standard_D4s_v5"
priority = "Spot"
eviction_policy = "Delete"
spot_max_price = -1
node_count = 0
min_count = 0
max_count = 20
enable_auto_scaling = true
node_taints = ["kubernetes.azure.com/scalesetpriority=spot:NoSchedule"]
}
CI/CD Pipeline with GitHub Actions (OIDC)
name: Terraform AKS Deploy
on:
push:
branches: [main]
paths: ["infra/**"]
permissions:
id-token: write
contents: read
jobs:
terraform:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- uses: hashicorp/setup-terraform@v3
- run: terraform init && terraform apply -auto-approve -var-file=environments/prd.tfvars
working-directory: infra
Best Practices and Pitfalls
- Never commit state: always use a remote backend (Azure Storage, Terraform Cloud)
- Pin versions: provider
~> 3.110and Terraform>= 1.6inrequired_providers - Separate node pools from the main cluster to avoid recreating the cluster on updates
- Avoid
lifecycle { ignore_changes }onnode_countwhen auto-scaling is enabled - Use
.tfvarsfiles per environment rather than Git branches
Conclusion
Terraform on Azure enables deploying a complete, secure, and repeatable AKS cluster in a few hundred lines of code. The combination of a custom VNet + Azure CNI Overlay + Workload Identity + Managed Prometheus is the production reference in 2025.
Move2Cloud supports its clients in setting up this IaC stack, from architecture design to CI/CD integration, with reusable Terraform modules and multi-environment governance.
