0%

Infrastructure as Code --- Terraform

This article focuses on an infrastructure as code tool, terraform, and helps developers quickly and confidently scale your cloud deployments, and also easily maintain and manage the infrastructure of their projects.

1. IAC Tool — Terraform

Terraform is an IAC tool created by Hashicorp, and it defines infrastructure (cloud, on-prem, SaaS) in declarative .tf files and provisions it. Terragrunt is a thin wrapper around Terraform, created by Gruntwork. It helps with managing Terraform projects at scale (especially with multiple environments, accounts, or modules).

1.1 Terraform

  • Core features:
  • Uses providers (AWS, Azure, GCP, etc.).
  • Manages resources (VMs, databases, networks, etc.).
  • Maintains state (in .tfstate files).
  • Supports modules for reusability.

✅ Best when: you want a direct, provider-backed tool to define and manage infrastructure.

1.2 Terragrunt

  • Core features:
  • DRY (Don’t Repeat Yourself): Centralizes common Terraform code and variables.
  • Handles remote state management easily (e.g., S3 + DynamoDB for AWS).
  • Manages multiple Terraform modules (e.g., prod, dev, staging).
  • Dependency management: ensures some stacks are applied before others.
  • Adds a standard project structure.

✅ Best when: you have many environments, modules, or teams and need orchestration and consistency.

1.3 Terraform and Terragrunt

  • Terraform is the engine: it does the actual work of provisioning.

  • Terragrunt is the orchestrator: it helps you run Terraform consistently across many environments with less repetition.

1.4 When to use what

  • Small project, single environment → Terraform only is fine.

  • Large project, multiple environments/accounts → Terraform + Terragrunt gives better structure and automation.

2. Example of Usage

2.1 Folder Structure

A common pattern is:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
infra-live/
├── terragrunt.hcl # Root configuration (backend, provider defaults)
├── dev/
│ ├── app/
│ │ └── terragrunt.hcl
│ └── network/
│ └── terragrunt.hcl
├── stage/
│ ├── app/
│ │ └── terragrunt.hcl
│ └── network/
│ └── terragrunt.hcl
├── prod/
│ ├── app/
│ │ └── terragrunt.hcl
│ └── network/
│ └── terragrunt.hcl
infra-modules/
├── network/
│ └── main.tf
├── app/
│ └── main.tf
  • infra-modules/ → reusable Terraform modules (network, app, etc.)

  • infra-live/Terragrunt configs per environment that call the modules


2.2 Root terragrunt.hcl

This defines common settings (remote state + provider configs) for all environments:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# infra-live/terragrunt.hcl

remote_state {
backend = "azurerm"
config = {
resource_group_name = "rg-terraform-state"
storage_account_name = "tfstatestorageacct"
container_name = "tfstate"
key = "${path_relative_to_include()}/terraform.tfstate"
}
}

generate "provider" {
path = "provider.tf"
if_exists = "overwrite"
contents = <<EOF
provider "azurerm" {
features {}
}
EOF
}

👉 What this does:

  • Stores Terraform state in an Azure Storage Account (remote backend).

  • Generates a provider block so we don’t need to repeat it in every module.


2.3 Module Example — network

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# infra-modules/network/main.tf

resource "azurerm_resource_group" "rg" {
name = var.resource_group_name
location = var.location
}

resource "azurerm_virtual_network" "vnet" {
name = var.vnet_name
address_space = ["10.0.0.0/16"]
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
}

variable "resource_group_name" {}

variable "location" {}

variable "vnet_name" {}

2.4 Terragrunt Environment Config — Dev Network

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# infra-live/dev/network/terragrunt.hcl

include {
path = find_in_parent_folders()
}

terraform {
source = "../../../infra-modules/network"
}

inputs = {
resource_group_name = "rg-dev"
location = "eastus"
vnet_name = "vnet-dev"
}

👉 Breakdown:

  • include {} → pulls in root terragrunt.hcl (so backend/provider settings apply).

  • terraform { source = ... } → points to the Terraform module.

  • inputs {} → values for this environment.


2.5 Terragrunt Environment Config — Dev App

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# infra-live/dev/app/terragrunt.hcl

include {
path = find_in_parent_folders()
}

terraform {
source = "../../../infra-modules/app"
}

dependencies {
paths = ["../network"]
}

inputs = {
resource_group_name = "rg-dev"
location = "eastus"
app_name = "myapp-dev"
}

👉 Breakdown:

  • Declares dependency on network (so VNet is created first).

  • Uses same provider/backend from root.

  • Environment-specific app name.


2.6 Running Terragrunt

Commands are the same as Terraform but scoped:

1
2
3
4
5
6
7
cd infra-live/dev/network

terragrunt init

terragrunt plan

terragrunt apply

Or run for all modules in an environment:

1
2
3
4
5
cd infra-live/dev

terragrunt run-all plan

terragrunt run-all apply

3. 🔑 Key Syntax & Behavior

  • include {} → inherit root config (keeps DRY).

  • terraform { source = ... } → points to a reusable Terraform module.

  • inputs = {} → variables passed to the module.

  • dependencies {} → ensures correct order across stacks.

  • remote_state {} → centralizes state storage in Azure.

  • generate {} → dynamically writes files (like provider.tf).