
Automated Resource Deployment via Terraform
Overview of Terraform
Terraform is an open‑source tool for building, changing and versioning infrastructure safely and efficiently. It uses a declarative configuration language (HashiCorp Configuration Language, HCL) to describe the desired state of your infrastructure. When applied, Terraform generates an execution plan showing what it will do to reach that state and then executes the plan to provision, update or destroy resources across a variety of cloud providers.
Key features of Terraform:
- Declarative syntax
- Define “what” you want, not “how” to get there.
- Provider ecosystem
- First‑class support for Azure through the AzureRM provider.
- State management
- Keeps track of real‑world resources in a state file, enabling powerful plans and diffs.
- Modules
- Reusable, composable units of configuration that promote DRY and consistency.
- Workspaces
- Handle multiple environments (for example dev, test, prod) in a single configuration.
General Best Practices
- Use version control
- Store all Terraform code in Git or another VCS. Review changes via pull requests to catch mistakes early.
- Lock provider and Terraform Versions
- Specify exact versions to avoid unexpected changes when new versions are released:
terraform {
required_version = ">= 1.5.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "=4.50.0"
}
}
}
- Use modules for reuse and clarity
- Break down your infrastructure into logical modules, each handling one responsibility (for example resource group, service plan, web app, private endpoint).
- Group variables and outputs
- Define all variables in
variables.tffiles with clear descriptions, types and defaults. Expose needed values viaoutputs.tf. - Manage secrets securely
- Do not commit sensitive values in Git. Use Azure Key Vault or environment variables and mark variables as
sensitive = true. - Tag resources consistently
- Apply tags such as
environment,managed_by, andcost_centerto all resources for governance and cost tracking. - Use linting and formatting tools**
- Run
terraform fmtandterraform validatein CI pipelines to enforce style and detect errors early.
Terraform Project Structure
Below is the recommended layout for a Terraform project deploying Azure resources:
terraform/
├── main.tf ← root module: providers + module calls
├── variables.tf ← all your var definitions
├── providers.tf ← provider + terraform blocks (optional split)
├── terraform.tfvars ← your actual var values
└── modules/
├── resource_group/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
├── service_plan/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
├── linux_web_app/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
└── private_endpoint/
├── main.tf
├── variables.tf
└── outputs.tf
Root Module
main.tf
- Purpose
-
Orchestrates module calls and defines resource dependencies.
-
Best Practices
- Keep it focused on wiring up modules.
- Make use of
depends_ononly when implicit dependencies are insufficient
module "rg" {
source = "./modules/resource_group"
name = var.rg_name
location = var.location
}
module "plan" {
source = "./modules/service_plan"
rg_name = module.rg.name
app_service_tier = var.app_service_tier
}
module "webapp" {
source = "./modules/linux_web_app"
rg_name = module.rg.name
service_plan = module.plan.name
app_name = var.app_name
}
module "endpoint" {
source = "./modules/private_endpoint"
rg_name = module.rg.name
virtual_network = var.virtual_network
subnet_name = var.subnet_name
webapp_resource_id = module.webapp.id
}
variables.tf
- Purpose
-
Declare all inputs for the root module.
-
Best Practices
- Provide
description,type, and sensible defaults - Mark truly optional settings with defaults; require mandatory ones without defaults.
variable "rg_name" {
description = "Name of the resource group"
type = string
}
variable "location" {
description = "Azure region"
type = string
default = "eastus2"
}
variable "app_name" {
description = "Name for the Linux web app"
type = string
}
providers.hcl
- Purpose
-
Configure the Terraform backend and Azure provider.
-
Best Practices
- Split if you have multiple providers or to separate concerns.
terraform {
required_version = ">= 1.5.0"
backend "azurerm" {
resource_group_name = "tfstate-rg"
storage_account_name = "tfstate"
container_name = "state"
key = "prod.terraform.tfstate"
}
}
provider "azurerm" {
features {}
}
terraform.tfvars
- Purpose
-
Store actual values for variables (do not commit secrets).
-
Best Practices
- Add this file to
.gitignore. - For secrets, consider using a separate
secrets.auto.tfvarsor environment variables.
rg_name = "my-app-rg"
location = "eastus2"
app_name = "my-linux-webapp"
app_service_tier = "Standard_S1"
virtual_network = "my-vnet"
subnet_name = "web-subnet"
Modules
Each module follows a consistent pattern with main.tf, variables.tf and outputs.tf.
Example Module - Resource Group
modules/resource_group
main.tf
resource "azurerm_resource_group" "this" {
name = var.name
location = var.location
tags = var.tags
}
variables.tf- require
nameandlocation - Accept a
tagsmap with an empty default value
variable "name" {
description = "Resource group name"
type = string
}
variable "location" {
description = "Azure region"
type = string
}
variable "tags" {
description = "Resource tags"
type = map(string)
default = {}
}
outputs.tf
output "name" {
description = "Name of the resource group"
value = azurerm_resource_group.this.name
}