Back to Blog
3 min read

Infrastructure as Code Best Practices: Lessons from 100+ Deployments

After managing over 100 production deployments with Infrastructure as Code in 2025, these are the practices that consistently prevented problems and saved time.

1. Use Modules Religiously

Never repeat infrastructure definitions. Create reusable modules:

# modules/azure-function/main.tf
variable "name" {
  type = string
}

variable "resource_group_name" {
  type = string
}

variable "app_settings" {
  type    = map(string)
  default = {}
}

resource "azurerm_linux_function_app" "function" {
  name                = var.name
  resource_group_name = var.resource_group_name
  location            = data.azurerm_resource_group.rg.location

  service_plan_id            = azurerm_service_plan.plan.id
  storage_account_name       = azurerm_storage_account.storage.name
  storage_account_access_key = azurerm_storage_account.storage.primary_access_key

  site_config {
    application_stack {
      dotnet_version = "8.0"
    }
  }

  app_settings = merge({
    "FUNCTIONS_WORKER_RUNTIME" = "dotnet-isolated"
  }, var.app_settings)
}

# Usage
module "order_processor" {
  source              = "./modules/azure-function"
  name                = "order-processor-${var.environment}"
  resource_group_name = azurerm_resource_group.main.name
  app_settings = {
    "ServiceBusConnection" = azurerm_servicebus_namespace.main.default_primary_connection_string
  }
}

2. Environment Separation with Workspaces

# environments/dev.tfvars
environment = "dev"
sku_tier    = "Basic"
replicas    = 1

# environments/prod.tfvars
environment = "prod"
sku_tier    = "Premium"
replicas    = 3

# main.tf
resource "azurerm_service_plan" "plan" {
  name     = "plan-${var.environment}"
  sku_name = var.sku_tier == "Premium" ? "P1v3" : "B1"
}

3. State Management

Always use remote state with locking:

terraform {
  backend "azurerm" {
    resource_group_name  = "terraform-state-rg"
    storage_account_name = "tfstateprod"
    container_name       = "tfstate"
    key                  = "project.tfstate"
  }
}

4. Naming Conventions

Consistent naming prevents confusion:

locals {
  name_prefix = "${var.project}-${var.environment}-${var.region_short}"

  naming = {
    resource_group   = "rg-${local.name_prefix}"
    storage_account  = "st${replace(local.name_prefix, "-", "")}"
    key_vault        = "kv-${local.name_prefix}"
    function_app     = "func-${local.name_prefix}"
  }
}

5. Validation Before Apply

variable "environment" {
  type = string
  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "Environment must be dev, staging, or prod."
  }
}

variable "sku_tier" {
  type = string
  validation {
    condition     = var.environment != "prod" || var.sku_tier == "Premium"
    error_message = "Production must use Premium tier."
  }
}

6. Output Everything Important

output "function_app_url" {
  value       = "https://${azurerm_linux_function_app.function.default_hostname}"
  description = "The URL of the deployed function app"
}

output "connection_strings" {
  value = {
    storage     = azurerm_storage_account.storage.primary_connection_string
    service_bus = azurerm_servicebus_namespace.main.default_primary_connection_string
  }
  sensitive = true
}

7. Use Data Sources for Dependencies

# Reference existing resources instead of hardcoding
data "azurerm_client_config" "current" {}

data "azurerm_key_vault" "shared" {
  name                = "kv-shared-${var.environment}"
  resource_group_name = "rg-shared-${var.environment}"
}

resource "azurerm_key_vault_access_policy" "function" {
  key_vault_id = data.azurerm_key_vault.shared.id
  tenant_id    = data.azurerm_client_config.current.tenant_id
  object_id    = azurerm_linux_function_app.function.identity[0].principal_id

  secret_permissions = ["Get", "List"]
}

Key Principles

  1. Immutable infrastructure - Replace, don’t modify
  2. Everything in code - No manual portal changes
  3. Review before apply - Use terraform plan in CI/CD
  4. Test in lower environments - Never apply to prod first
  5. Document with comments - Future you will thank present you

IaC is a skill that compounds. These practices become second nature and save countless hours of debugging and recovery.

Michael John Peña

Michael John Peña

Senior Data Engineer based in Sydney. Writing about data, cloud, and technology.