Fatskills
Practice. Master. Repeat.
Study Guide: TECH **Terraform Modules: Passing Variables & Accessing Outputs – Zero-Fluff Study Guide**
Source: https://www.fatskills.com/cloud-application-developer/chapter/tech-terraform-modules-passing-variables-accessing-outputs-zero-fluff-study-guide

TECH **Terraform Modules: Passing Variables & Accessing Outputs – Zero-Fluff Study Guide**

By Fatskills Exam Guides Team — the exam nerds behind 28,500+ quizzes and 2.1M practice questions across 500+ global exams.

⏱️ ~8 min read

Terraform Modules: Passing Variables & Accessing Outputs – Zero-Fluff Study Guide


1. What This Is & Why It Matters

You’re building a Terraform project to deploy a multi-environment AWS infrastructure (dev, staging, prod). Each environment needs: - A VPC with different CIDR blocks - EC2 instances with varying sizes - RDS databases with different storage and instance types

Without modules, you’d copy-paste the same main.tf three times, manually changing values. With modules, you define the infrastructure once and parameterize it—passing different variables for each environment.

Why this matters in production:
- DRY (Don’t Repeat Yourself): One change fixes all environments.
- Consistency: No more "works in dev but fails in prod" because of a typo.
- Security: Sensitive values (like DB passwords) can be passed securely via variables.
- Scalability: Add a new environment (e.g., qa) in minutes, not hours.

Real-world scenario:
You inherit a Terraform repo where someone hardcoded instance_type = "t2.micro" in 15 places. Your boss says, "We need to upgrade all prod instances to t3.medium." Without modules, you’d manually edit 15 files. With modules, you change one variable.


2. Core Concepts & Components


1. module Block

  • Definition: A reusable Terraform configuration that encapsulates resources.
  • Production insight: Modules should be versioned (e.g., source = "git::https://github.com/org/vpc-module?ref=v1.2.0") to avoid breaking changes.

2. variable Block (Input Variables)

  • Definition: Parameters passed into a module (or root config) to customize behavior.
  • Production insight: Always validate variables (e.g., validation { condition = var.instance_type == "t3.micro" || var.instance_type == "t3.small" }) to catch misconfigurations early.

3. output Block (Module Outputs)

  • Definition: Values exposed by a module for use in other parts of Terraform.
  • Production insight: Outputs are critical for chaining modules (e.g., passing an RDS endpoint to an EC2 user-data script).

4. locals (Local Values)

  • Definition: Variables computed inside a module (not passed in).
  • Production insight: Use locals for derived values (e.g., local.subnet_cidr = "${var.vpc_cidr}.0.0/24") to avoid repetition.

5. terraform.tfvars (Variable Definitions)

  • Definition: A file where you set variable values (e.g., instance_type = "t3.medium").
  • Production insight: Never commit terraform.tfvars to Git if it contains secrets (use *.tfvars in .gitignore).

6. -var & -var-file (CLI Variable Overrides)

  • Definition: Pass variables via CLI (e.g., terraform apply -var="instance_type=t3.large").
  • Production insight: Useful for CI/CD pipelines where you want to override values per environment.

7. sensitive = true (Sensitive Outputs)

  • Definition: Marks an output as sensitive (Terraform won’t show it in logs).
  • Production insight: Always mark DB passwords, API keys, and private IPs as sensitive.

8. depends_on (Explicit Dependencies)

  • Definition: Forces Terraform to wait for one resource before creating another.
  • Production insight: Rarely needed, but critical for IAM roles + resources (e.g., an EC2 instance must wait for an IAM role to exist).


3. Step-by-Step Hands-On: Deploy a VPC with Modules


Prerequisites

  • AWS account with admin IAM permissions
  • Terraform installed (terraform --version should work)
  • AWS CLI configured (aws sts get-caller-identity should return your user)

Goal

Deploy a VPC with public/private subnets using a module, passing variables for: - CIDR block - Subnet sizes - Environment name (for tagging)

Step 1: Create the Module Structure

mkdir -p terraform-modules/vpc
cd terraform-modules/vpc
touch main.tf variables.tf outputs.tf

Step 2: Define the Module (main.tf)

# terraform-modules/vpc/main.tf
resource "aws_vpc" "this" {
  cidr_block           = var.vpc_cidr
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = {
Name = "${var.environment}-vpc"
Environment = var.environment } } resource "aws_subnet" "public" { count = length(var.public_subnet_cidrs) vpc_id = aws_vpc.this.id cidr_block = var.public_subnet_cidrs[count.index] availability_zone = data.aws_availability_zones.available.names[count.index] map_public_ip_on_launch = true tags = {
Name = "${var.environment}-public-subnet-${count.index}"
Environment = var.environment } } resource "aws_subnet" "private" { count = length(var.private_subnet_cidrs) vpc_id = aws_vpc.this.id cidr_block = var.private_subnet_cidrs[count.index] availability_zone = data.aws_availability_zones.available.names[count.index] tags = {
Name = "${var.environment}-private-subnet-${count.index}"
Environment = var.environment } } data "aws_availability_zones" "available" { state = "available" }

Step 3: Define Input Variables (variables.tf)

# terraform-modules/vpc/variables.tf
variable "vpc_cidr" {
  description = "CIDR block for the VPC"
  type        = string
  default     = "10.0.0.0/16"
}

variable "public_subnet_cidrs" {
  description = "List of CIDR blocks for public subnets"
  type        = list(string)
  default     = ["10.0.1.0/24", "10.0.2.0/24"]
}

variable "private_subnet_cidrs" {
  description = "List of CIDR blocks for private subnets"
  type        = list(string)
  default     = ["10.0.3.0/24", "10.0.4.0/24"]
}

variable "environment" {
  description = "Environment name (e.g., dev, prod)"
  type        = string
  default     = "dev"
}

Step 4: Define Outputs (outputs.tf)

# terraform-modules/vpc/outputs.tf
output "vpc_id" {
  description = "ID of the created VPC"
  value       = aws_vpc.this.id
}

output "public_subnet_ids" {
  description = "IDs of the public subnets"
  value       = aws_subnet.public[*].id
}

output "private_subnet_ids" {
  description = "IDs of the private subnets"
  value       = aws_subnet.private[*].id
}

Step 5: Use the Module in Root Config

cd ../..
mkdir -p environments/dev cd environments/dev touch main.tf terraform.tfvars

main.tf (Root Config)

# environments/dev/main.tf
terraform {
  required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
} } } provider "aws" { region = "us-east-1" } module "vpc" { source = "../../terraform-modules/vpc" vpc_cidr = var.vpc_cidr public_subnet_cidrs = var.public_subnet_cidrs private_subnet_cidrs = var.private_subnet_cidrs environment = var.environment }

terraform.tfvars (Variable Values)

# environments/dev/terraform.tfvars
vpc_cidr             = "10.1.0.0/16"
public_subnet_cidrs  = ["10.1.1.0/24", "10.1.2.0/24"]
private_subnet_cidrs = ["10.1.3.0/24", "10.1.4.0/24"]
environment          = "dev"

Step 6: Deploy the Infrastructure

terraform init
terraform plan
terraform apply -auto-approve

Step 7: Verify Outputs

terraform output

Expected Output:


vpc_id = "vpc-1234567890abcdef0"
public_subnet_ids = [
  "subnet-1234567890abcdef0",
  "subnet-0987654321fedcba0",
]
private_subnet_ids = [
  "subnet-abcdef12345678900",
  "subnet-fedcba09876543210",
]

Step 8: Use Module Outputs in Another Module

Let’s say you want to deploy an EC2 instance in a public subnet. You can reference the VPC module’s outputs:


# environments/dev/main.tf (add this)
module "ec2" {
  source        = "../../terraform-modules/ec2"
  subnet_id     = module.vpc.public_subnet_ids[0]  # Use first public subnet
  instance_type = "t3.micro"
  environment   = var.environment
}


4. ? Production-Ready Best Practices


Security

  • Never hardcode secrets in modules. Use:
  • sensitive = true for outputs
  • AWS Secrets Manager / Parameter Store for DB passwords
  • terraform.tfvars in .gitignore for sensitive values
  • Validate inputs to prevent misconfigurations: hcl variable "instance_type" {
    type = string
    validation {
    condition = contains(["t3.micro", "t3.small", "t3.medium"], var.instance_type)
    error_message = "Instance type must be t3.micro, t3.small, or t3.medium."
    } }

Cost Optimization

  • Use count or for_each to conditionally create resources: hcl resource "aws_instance" "example" {
    count = var.environment == "prod" ? 2 : 1
    instance_type = var.instance_type }
  • Tag resources for cost allocation: hcl tags = {
    Environment = var.environment
    CostCenter = "12345" }

Reliability & Maintainability

  • Version modules (e.g., source = "git::https://github.com/org/vpc-module?ref=v1.2.0").
  • Use locals for derived values to avoid repetition: hcl locals {
    subnet_cidr = "${var.vpc_cidr}.0.0/24" }
  • Name resources consistently:
  • Bad: aws_instance.web_server
  • Good: aws_instance.web_server_${var.environment}

Observability

  • Output critical values (e.g., RDS endpoints, ALB DNS names): hcl output "alb_dns_name" {
    value = aws_lb.this.dns_name }
  • Use terraform state list to audit resources: bash terraform state list


5. ⚠️ Common Mistakes & Traps

Mistake Symptom Fix/Prevention
Hardcoding values in modules Can’t reuse module for different environments. Always use variables.
Forgetting sensitive = true DB password appears in terraform output. Mark sensitive outputs.
Not versioning modules Breaking changes when someone updates the module. Use ref=v1.2.0 in source.
Passing wrong variable types Error: Incorrect attribute value type Define type = string/list/map in variables.
Ignoring depends_on IAM role not ready when EC2 launches. Explicitly declare dependencies.


6. ? Exam/Certification Focus


Typical Question Patterns

  1. "How do you pass a variable to a module?"
  2. Correct: module "vpc" { vpc_cidr = "10.0.0.0/16" }
  3. Wrong: Hardcoding in the module.

  4. "How do you access a module’s output?"

  5. Correct: module.vpc.vpc_id
  6. Wrong: aws_vpc.this.id (only works inside the module).

  7. "What’s the difference between variable and locals?"

  8. Correct: variable is passed in; locals are computed inside the module.

Key ⚠️ Trap Distinctions

  • terraform.tfvars vs. variables.tf
  • variables.tf = defines variables (type, description).
  • terraform.tfvars = sets variable values.

  • count vs. for_each

  • count = index-based (e.g., aws_subnet.public[0]).
  • for_each = key-based (e.g., aws_subnet.public["us-east-1a"]).


7. ? Hands-On Challenge

Challenge:
Create a module that deploys an S3 bucket with: - A variable for the bucket name (default: my-bucket-${random_id.suffix.hex}) - A variable for versioning (default: false) - An output for the bucket ARN

Solution:


# modules/s3/main.tf
resource "random_id" "suffix" {
  byte_length = 4
}

resource "aws_s3_bucket" "this" {
  bucket = "${var.bucket_name}-${random_id.suffix.hex}"
  versioning {
enabled = var.versioning } } # modules/s3/variables.tf variable "bucket_name" { type = string default = "my-bucket" } variable "versioning" { type = bool default = false } # modules/s3/outputs.tf output "bucket_arn" { value = aws_s3_bucket.this.arn }

Why it works:
- random_id ensures unique bucket names.
- versioning is configurable via a variable.
- The ARN is exposed for other modules to use.


8. ? Rapid-Reference Crib Sheet

Command/Concept Usage Notes
module "name" { ... } Define a module source must point to a local path or Git repo.
variable "name" { ... } Declare an input variable Always set type and description.
output "name" { ... } Expose a module’s value Use sensitive = true for secrets.
terraform output List all outputs Add -json for machine-readable format.
terraform apply -var="key=value" Override a variable Useful in CI/CD pipelines.
locals { ... } Compute values inside a module Avoids repetition.
depends_on = [resource] Force dependency order Rarely needed, but critical for IAM.
*.tfvars Set variable values Never commit secrets!
terraform state list Audit deployed resources Useful for debugging.


9. ? Where to Go Next

  1. Terraform Modules Documentation
  2. AWS VPC Module (Official)
  3. Terraform Best Practices (GitHub)
  4. Terraform Up & Running (Book) – Chapter 4 covers modules in depth.

Final Tip:


"Modules are like Lego blocks. Each block does one thing well. Combine them to build anything—just don’t glue them together permanently."


Now go build something! ?



ADVERTISEMENT