100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Terraform & Infrastructure as Code
50 minintermediate

Variables Practice — Parameterized Config

What You'll Build

You will transform a hardcoded Terraform configuration for the CricketPulse networking layer into a fully parameterised, multi-environment configuration. Starting from a configuration with hardcoded values, you will extract variables with appropriate types and validation, compute subnet CIDRs dynamically using `cidrsubnet()`, use `for_each` to create all subnets from a map variable, add conditional resources for production-only features, implement meaningful outputs, and create dev and production tfvars files. The result will be a single configuration deployable to both environments with environment-appropriate sizing, subnets, and features.

Analogy🏏Cricket
🏏 Think of it like cricket: This exercise is your first bat at the nets — not a match, but the first real contact between bat and ball. You're not yet managing complex match strategies or multi-ground tournaments; you're simply practicing the forward defensive — the fundamental stroke. Provisioning a single VM with its network prerequisites is the forward defensive of Terraform: not glamorous, but the foundation of every subsequent shot. Each command (init, plan, apply) is a distinct movement to practise until it becomes automatic, because every future Terraform operation builds on this same three-step sequence.

Prerequisites

  • Terraform 1.5+ installed with AWS provider configured (or LocalStack for local testing).
  • AWS credentials configured — either via `aws configure` or environment variables.
  • Lessons 9–11 completed — understanding of variables, data sources, functions, and for_each.
  • The completed configuration from Lesson 8 (remote backend setup) or a fresh directory with `terraform init`.
  • Familiarity with basic networking concepts — CIDR blocks, subnets, public vs private routing.

Setup & Project Structure

Create the project structure with separate files for variables, locals, resources, and outputs — this is the conventional Terraform file organisation that makes large configurations navigable.

Analogy🏏Cricket
🏏 Think of it like cricket: setting up separate files for providers, network, and compute is like organising your kit bag before a match with distinct compartments — pads in one, gloves in another, bat and helmet each in their place. Just as a well-ordered kit bag lets you grab exactly the gear you need without rummaging, splitting your Terraform config by concern lets you find the network or provider settings instantly. Just as every player knows the batting order is written down so nobody argues about who's next in, the file-per-concern convention means any teammate reading your CricketPulse config knows precisely where the compute or networking lives. And just as a tidy kit bag survives a chaotic changeroom, a readable structure survives a growing configuration. The payoff: like a professional who never scrambles for a missing glove at the crease, you keep the config navigable and calm as the infrastructure grows.
bash
mkdir -p cricketpulse-networking
cd cricketpulse-networking

# Create the conventional file structure
touch main.tf variables.tf locals.tf outputs.tf
touch terraform.tfvars production.tfvars

# Initial main.tf with provider config
cat > main.tf << 'EOF'
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

# Data sources for dynamic values
data "aws_availability_zones" "available" {
  state = "available"
}
data "aws_region" "current" {}
EOF

terraform init
echo 'Project initialised'

Step 1 — Foundation

Define all input variables with types, descriptions, defaults, and validation rules. This is the variable layer that callers use to customise the configuration — getting these right from the start saves significant rework later.

Analogy🏏Cricket
🏏 Think of it like cricket: Step 1 builds the ground infrastructure before the match begins. The VPC is the stadium boundary walls — the outer container that defines the playing area. The subnet is the designated field within the boundary. The internet gateway is the public entrance gate that allows spectators (traffic) to enter. You build the ground infrastructure before placing the players (EC2 instances) on the field.
hcl
cat > variables.tf << 'EOF'
variable "environment" {
  type        = string
  description = "Deployment environment: dev, staging, or production"
  validation {
    condition     = contains(["dev", "staging", "production"], var.environment)
    error_message = "Environment must be dev, staging, or production."
  }
}

variable "aws_region" {
  type        = string
  description = "AWS region to deploy CricketPulse networking"
  default     = "ap-south-1"
}

variable "project" {
  type        = string
  description = "Project name used as resource name prefix"
  default     = "cricketpulse"
}

variable "vpc_cidr" {
  type        = string
  description = "CIDR block for the CricketPulse VPC (e.g. 10.0.0.0/16)"
  default     = "10.0.0.0/16"
  validation {
    condition     = can(cidrhost(var.vpc_cidr, 0))
    error_message = "VPC CIDR must be a valid CIDR notation."
  }
}

variable "az_count" {
  type        = number
  description = "Number of availability zones to use for subnets"
  default     = 2
  validation {
    condition     = var.az_count >= 1 && var.az_count <= 3
    error_message = "AZ count must be between 1 and 3."
  }
}

variable "enable_nat_gateway" {
  type        = bool
  description = "Whether to create NAT gateway(s) for private subnet internet access"
  default     = true
}

variable "enable_vpn_gateway" {
  type        = bool
  description = "Whether to create a VPN gateway (production only)"
  default     = false
}
EOF

Step 2 — Core Logic

Implement the locals for computed values and the main networking resources using `for_each` and dynamic subnet CIDR calculation. The locals layer is where the variable values are transformed into the exact values resources need.

Analogy🏏Cricket
🏏 Think of it like cricket: Step 2 is placing the players on the prepared ground. The security group is the access accreditation policy — which players (traffic sources) are allowed through which gates (ports). The EC2 instance is the opening batsman taking the crease. The apply command is the toss coin flip that starts the match — the moment the plan becomes real infrastructure.
hcl
cat > locals.tf << 'EOF'
locals {
  name_prefix  = "${var.project}-${var.environment}"
  common_tags  = {
    Project     = var.project
    Environment = var.environment
    ManagedBy   = "terraform"
  }
  is_production = var.environment == "production"

  # Calculate CIDRs dynamically from vpc_cidr
  az_list = slice(data.aws_availability_zones.available.names, 0, var.az_count)

  private_subnets = {
    for i, az in local.az_list :
    "${local.name_prefix}-private-${az}" => {
      cidr = cidrsubnet(var.vpc_cidr, 8, i)
      az   = az
      tier = "private"
    }
  }

  public_subnets = {
    for i, az in local.az_list :
    "${local.name_prefix}-public-${az}" => {
      cidr = cidrsubnet(var.vpc_cidr, 8, i + 100)
      az   = az
      tier = "public"
    }
  }
}
EOF

cat > main.tf << 'EOF'
terraform {
  required_providers {
    aws = { source = "hashicorp/aws", version = "~> 5.0" }
  }
}
provider "aws" { region = var.aws_region }
data "aws_availability_zones" "available" { state = "available" }
data "aws_region" "current" {}

resource "aws_vpc" "main" {
  cidr_block = var.vpc_cidr
  tags       = merge(local.common_tags, { Name = "${local.name_prefix}-vpc" })
}

resource "aws_subnet" "private" {
  for_each          = local.private_subnets
  vpc_id            = aws_vpc.main.id
  cidr_block        = each.value.cidr
  availability_zone = each.value.az
  tags = merge(local.common_tags, {
    Name = each.key
    Tier = each.value.tier
  })
}

resource "aws_subnet" "public" {
  for_each                = local.public_subnets
  vpc_id                  = aws_vpc.main.id
  cidr_block              = each.value.cidr
  availability_zone       = each.value.az
  map_public_ip_on_launch = true
  tags = merge(local.common_tags, {
    Name = each.key
    Tier = each.value.tier
  })
}

# Optional NAT gateway (conditional)
resource "aws_eip" "nat" {
  count  = var.enable_nat_gateway ? length(local.az_list) : 0
  domain = "vpc"
  tags   = merge(local.common_tags, { Name = "${local.name_prefix}-nat-eip-${count.index}" })
}
EOF

Step 3 — Integration & Enhancement

Add meaningful outputs and create the two tfvars files. The outputs expose the VPC ID, subnet IDs, and AZ information for downstream configurations. The tfvars files demonstrate how the same configuration deploys differently to dev and production.

Analogy🏏Cricket
🏏 Think of it like cricket: Step 3 is making a tactical adjustment mid-match — changing the batting position (instance type) of an already-fielded player. Some changes are in-place (the batsman changes their stance without leaving the crease — `~` in the plan). Others require the player to leave and re-enter (the player goes back to the pavilion and returns — `-/+` replace in the plan). Terraform's plan output tells you which type of change is required before you commit to it.
hcl
cat > outputs.tf << 'EOF'
output "vpc_id" {
  value       = aws_vpc.main.id
  description = "ID of the CricketPulse VPC"
}

output "private_subnet_ids" {
  value       = [for s in aws_subnet.private : s.id]
  description = "List of private subnet IDs"
}

output "public_subnet_ids" {
  value       = [for s in aws_subnet.public : s.id]
  description = "List of public subnet IDs"
}

output "availability_zones" {
  value       = local.az_list
  description = "Availability zones used"
}

output "network_summary" {
  value = {
    environment    = var.environment
    vpc_cidr       = var.vpc_cidr
    az_count       = var.az_count
    private_count  = length(aws_subnet.private)
    public_count   = length(aws_subnet.public)
    nat_enabled    = var.enable_nat_gateway
  }
}
EOF

# terraform.tfvars (dev — minimal cost)
cat > terraform.tfvars << 'EOF'
environment        = "dev"
vpc_cidr           = "10.1.0.0/16"
az_count           = 1
enable_nat_gateway = false
enable_vpn_gateway = false
EOF

# production.tfvars (production — full scale)
cat > production.tfvars << 'EOF'
environment        = "production"
vpc_cidr           = "10.0.0.0/16"
az_count           = 3
enable_nat_gateway = true
enable_vpn_gateway = true
EOF

Step 4 — Testing & Verification

Validate both environment configurations and observe how the same code produces different infrastructure plans based on the variable values.

Analogy🏏Cricket
🏏 Think of it like cricket: verifying the provisioned VM works, inspecting the state file, then destroying it cleanly is like the post-innings routine — confirm the scoreboard matches what happened on the field, review the official scorebook ball by ball, then clear and cover the ground so nothing is left running overnight. Just as you'd check the batsman's recorded runs actually reflect the shots played, you verify the infrastructure Terraform claims to have built is genuinely reachable and functioning. Just as the scorebook is the authoritative record you consult to settle any dispute, the state file is where you inspect exactly what Terraform tracks as existing. And just as a responsible side clears its kit and covers the pitch after stumps to avoid damage and cost, `terraform destroy` tears everything down cleanly so no resources keep billing you. The payoff: like a match properly closed out and the ground restored, you confirm the work, understand the record, and leave a clean slate behind.
bash
# Test variable validation
terraform plan -var="environment=invalid"
# Error: Environment must be dev, staging, or production.

# Plan for dev environment (default terraform.tfvars)
terraform plan
# Plan: 1 VPC + 1 private subnet + 1 public subnet + 0 NAT gateways = 3 to add

# Plan for production environment
terraform plan -var-file=production.tfvars
# Plan: 1 VPC + 3 private + 3 public + 3 EIPs + 3 NAT = 13 to add

# Check what outputs would be produced (dry run without apply)
terraform plan -var-file=production.tfvars -out=tfplan
terraform show tfplan | grep -A 5 'output'

# Validate the dynamic CIDRs are correct
terraform console
# > cidrsubnet("10.0.0.0/16", 8, 0)
# "10.0.0.0/24"
# > cidrsubnet("10.0.0.0/16", 8, 100)
# "10.0.100.0/24"
# > exit

echo 'Exercise verification complete'

Warning: The `cidrsubnet()` function calculates CIDRs based on the VPC CIDR and subnet index. If you change `var.vpc_cidr` in a running configuration, Terraform will plan to destroy and recreate ALL subnets (because their CIDRs change). Never change the VPC CIDR of a running environment — it requires creating a new VPC and migrating all resources. This is why the VPC CIDR should be set once and committed: `10.0.0.0/16` for production, `10.1.0.0/16` for staging, `10.2.0.0/16` for dev — distinct ranges that don't overlap and won't need changing.

Extension Challenge: Add a variable `var.tags` of type `map(string)` that allows callers to pass additional tags beyond the standard `common_tags`. Merge this with `local.common_tags` using `merge(local.common_tags, var.tags)` in all resources. Then add a variable `var.subnet_configs` of type `map(object({cidr_suffix=number, public=bool}))` that allows callers to define any number of subnets with arbitrary CIDR suffixes and public/private settings, replacing the current fixed private+public pattern. This makes the networking module truly generic and reusable across different CricketPulse deployment topologies.

  • Separate configuration into conventional files: `variables.tf`, `locals.tf`, `main.tf`, `outputs.tf` — this organisation makes large configurations navigable.
  • `cidrsubnet(vpc_cidr, 8, index)` dynamically computes subnet CIDRs from a VPC CIDR — change `az_count` and new subnets are automatically sized and addressed.
  • The `for i, az in local.az_list : key => {...}` pattern creates a map for `for_each` from a list of AZ names, enabling one resource instance per AZ.
  • `count = var.enable_feature ? 1 : 0` is the standard optional resource pattern — clean conditional inclusion without duplicate blocks.
  • Two tfvars files (`terraform.tfvars` for dev, `production.tfvars` for production) implement environment-specific configuration without duplicating the resource definitions.
  • Always validate variable values with `validation` blocks to catch misconfigurations before any API calls are made.
Lesson 12 of 24
0% complete