Terraform Modules and State
The two decisions that make or break a Terraform codebase are: how you split state and how you compose modules. Everything else is tabs vs spaces.
Repo layout that scales
Section titled “Repo layout that scales”Directoryinfra/
Directorymodules/
Directorynetwork/
- main.tf
- variables.tf
- outputs.tf
- versions.tf
Directorykubernetes/
- …
Directorypostgres/
- …
Directoryenvs/
Directorydev/
- main.tf
- terraform.tfvars
- backend.tf
Directorystaging/
- …
Directoryprod/
- …
- .tflint.hcl
- .terraform-docs.yml
A minimal module
Section titled “A minimal module”terraform { required_version = ">= 1.9.0" required_providers { azurerm = { source = "hashicorp/azurerm", version = "~> 4.0" } }}
variable "name" { type = string description = "Prefix for created resources."}
variable "address_space" { type = list(string) default = ["10.0.0.0/16"]}
resource "azurerm_virtual_network" "this" { name = "${var.name}-vnet" location = var.location resource_group_name = var.resource_group_name address_space = var.address_space tags = var.tags}
output "vnet_id" { value = azurerm_virtual_network.this.id}Remote state — backends
Section titled “Remote state — backends”terraform { backend "azurerm" { resource_group_name = "tfstate-rg" storage_account_name = "reetwiztfstate" container_name = "prod" key = "network.tfstate" use_azuread_auth = true }}terraform { backend "s3" { bucket = "reetwiz-tfstate-prod" key = "network/terraform.tfstate" region = "eu-west-1" dynamodb_table = "tfstate-lock" encrypt = true }}terraform { cloud { organization = "reetwiz" workspaces { name = "prod-network" } }}The three composition patterns
Section titled “The three composition patterns”- Root-per-env (recommended). One root module per env, calls shared child modules.
- Workspaces. One root, many
terraform workspaces. Simple, but env drift is easy — aterraform applyon the wrong workspace is production. Avoid for anything past 2 envs. - Terragrunt / stacks. External tool wraps the DRY problem. Powerful, another moving part.
Import + refactor without downtime
Section titled “Import + refactor without downtime”Terraform 1.5+ has native import blocks — you no longer need terraform import:
import { to = azurerm_resource_group.core id = "/subscriptions/xxx/resourceGroups/core-rg"}
resource "azurerm_resource_group" "core" { name = "core-rg" location = "westeurope"}Then terraform plan -generate-config-out=generated.tf and you get the HCL for free.