TFLAB22-Writing Your First Terraform Module

Writing Your First Terraform Module

🔧 Terraform Core ⭐ Intermediate module Module Structure source Module Inputs/Outputs

Scenario

You've been copying the same S3 bucket + versioning + encryption config across 6 different projects. Every copy has slight variations and bugs. You want to package this reusable pattern into a module — define it once, call it many times with different inputs.

Your Objectives
  • Create a local module in modules/s3-bucket/ with main.tf, variables.tf, outputs.tf.
  • The module should create an S3 bucket with versioning and encryption, accepting inputs for name, environment, and versioning toggle.
  • Call the module from the root config using module {} with source = "./modules/s3-bucket".
  • Call the module twice with different inputs to create two buckets.
  • Reference module outputs in the root config's outputs.

Additional Context

A module is just a directory with .tf files. The directory you run terraform apply from is the "root module." Any directory you reference with source is a "child module."

Modules communicate through inputs (variables) and outputs. The calling module passes values as arguments; the child module exposes results through outputs. This is the only interface — resources inside a module are not directly accessible from outside.

modules/s3-bucket/variables.tf

# Module inputs — the interface callers use
variable "bucket_name" {
  description = "Name for the S3 bucket."
  type        = string
}

variable "environment" {
  description = "Environment label (dev, staging, prod)."
  type        = string
  default     = "dev"
}

variable "enable_versioning" {
  description = "Toggle bucket versioning."
  type        = bool
  default     = true
}

modules/s3-bucket/main.tf

# Module resources — the implementation
resource "aws_s3_bucket" "this" {
  bucket = var.bucket_name

  tags = {
    Name        = var.bucket_name
    Environment = var.environment
    ManagedBy   = "terraform"
  }
}

resource "aws_s3_bucket_versioning" "this" {
  bucket = aws_s3_bucket.this.id
  versioning_configuration {
    status = var.enable_versioning ? "Enabled" : "Suspended"
  }
}

resource "aws_s3_bucket_server_side_encryption_configuration" "this" {
  bucket = aws_s3_bucket.this.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}

modules/s3-bucket/outputs.tf

# Module outputs — exposed to the calling module
output "bucket_id"  { value = aws_s3_bucket.this.id }
output "bucket_arn" { value = aws_s3_bucket.this.arn }
output "bucket_region" { value = aws_s3_bucket.this.region }

main.tf (root module)

# Root module — calls the s3-bucket module twice

terraform {
  required_version = ">= 1.5.0"
  required_providers {
    aws = { source = "hashicorp/aws", version = "~> 5.0" }
  }
}

provider "aws" { region = "us-east-1" }
data "aws_caller_identity" "current" {}

# First call — application data bucket
module "app_bucket" {
  source = "./modules/s3-bucket"

  bucket_name       = "app-data-${data.aws_caller_identity.current.account_id}"
  environment       = "dev"
  enable_versioning = true
}

# Second call — logs bucket (different inputs)
module "logs_bucket" {
  source = "./modules/s3-bucket"

  bucket_name       = "app-logs-${data.aws_caller_identity.current.account_id}"
  environment       = "dev"
  enable_versioning = false   # No versioning for logs
}

outputs.tf (root module)

# Reference module outputs with: module.MODULE_NAME.OUTPUT_NAME
output "app_bucket_name" { value = module.app_bucket.bucket_id }
output "app_bucket_arn"  { value = module.app_bucket.bucket_arn }
output "logs_bucket_name"{ value = module.logs_bucket.bucket_id }
output "logs_bucket_arn" { value = module.logs_bucket.bucket_arn }

Workflow Commands

# Create the module directory structure
mkdir -p modules/s3-bucket

terraform init    # Downloads providers and registers the local module
terraform plan    # Shows 6 resources (3 per module call)
terraform apply

terraform destroy
✓ Expected Output After Apply
Apply complete! Resources: 6 added, 0 changed, 0 destroyed.

Outputs:

app_bucket_arn   = "arn:aws:s3:::app-data-123456789012"
app_bucket_name  = "app-data-123456789012"
logs_bucket_arn  = "arn:aws:s3:::app-logs-123456789012"
logs_bucket_name = "app-logs-123456789012"