1 min read
Infrastructure as Code Best Practices: Lessons from 100+ Deployments
I wrote “Infrastructure as Code Best Practices: Lessons from 100+ Deployments” to share practical, production-minded guidance on this topic.
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
- Immutable infrastructure - Replace, don’t modify
- Everything in code - No manual portal changes
- Review before apply - Use
terraform planin CI/CD - Test in lower environments - Never apply to prod first
- 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.\n\n## Takeaways\n\nAdd a concise, personal takeaway and recommended next steps here.\n