Terraform on Ubuntu 26.04 LTS - sudo-rs, APT Rollback, and Hardened Base Images
Install and run Terraform on Ubuntu 26.04 LTS Resolute Raccoon. Covers sudo-rs as default, APT 3.2 rollback, Kernel 7.0, Wayland-only, ROCm, and building...
DevOps
Write reusable Terraform modules with proper structure, inputs, outputs, and versioning. Covers local modules, registry modules, module composition
A module is a directory containing .tf files. That's it. Every Terraform project is already a module (the "root module"). When you call one module from another, it becomes a "child module."
project/
├── main.tf ← Root module
├── variables.tf
├── outputs.tf
└── modules/
└── vpc/ ← Child module
├── main.tf
├── variables.tf
└── outputs.tfModules let you write infrastructure once and reuse it across projects, environments, and teams.
module "vpc" {
source = "./modules/vpc"
vpc_cidr = "10.0.0.0/16"
environment = "production"
project_name = "myapp"
}
# Use module outputs
resource "aws_instance" "web" {
subnet_id = module.vpc.public_subnet_ids[0]
}module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0"
name = "my-vpc"
cidr = "10.0.0.0/16"
azs = ["us-east-1a", "us-east-1b"]
private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24"]
enable_nat_gateway = true
}module "vpc" {
source = "git::https://gitlab.com/myorg/terraform-modules.git//vpc?ref=v1.2.0"
vpc_cidr = "10.0.0.0/16"
}modules/vpc/
├── main.tf # Resources
├── variables.tf # Inputs
├── outputs.tf # Outputs
├── versions.tf # Provider requirements
└── README.md # Documentationvariable "vpc_cidr" {
description = "CIDR block for the VPC"
type = string
default = "10.0.0.0/16"
validation {
condition = can(cidrhost(var.vpc_cidr, 0))
error_message = "Must be a valid CIDR block."
}
}
variable "environment" {
description = "Environment name (dev, staging, prod)"
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Must be dev, staging, or prod."
}
}
variable "public_subnet_count" {
description = "Number of public subnets"
type = number
default = 2
}
variable "private_subnet_count" {
description = "Number of private subnets"
type = number
default = 2
}
variable "enable_nat_gateway" {
description = "Enable NAT Gateway for private subnets"
type = bool
default = true
}
variable "tags" {
description = "Additional tags for all resources"
type = map(string)
default = {}
}data "aws_availability_zones" "available" {
state = "available"
}
locals {
azs = slice(data.aws_availability_zones.available.names, 0, max(var.public_subnet_count, var.private_subnet_count))
common_tags = merge(var.tags, {
Environment = var.environment
ManagedBy = "terraform"
})
}
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = merge(local.common_tags, {
Name = "${var.environment}-vpc"
})
}
resource "aws_subnet" "public" {
count = var.public_subnet_count
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index)
availability_zone = local.azs[count.index % length(local.azs)]
map_public_ip_on_launch = true
tags = merge(local.common_tags, {
Name = "${var.environment}-public-${local.azs[count.index % length(local.azs)]}"
Tier = "public"
})
}
resource "aws_subnet" "private" {
count = var.private_subnet_count
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index + 100)
availability_zone = local.azs[count.index % length(local.azs)]
tags = merge(local.common_tags, {
Name = "${var.environment}-private-${local.azs[count.index % length(local.azs)]}"
Tier = "private"
})
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = merge(local.common_tags, {
Name = "${var.environment}-igw"
})
}output "vpc_id" {
description = "ID of the VPC"
value = aws_vpc.main.id
}
output "vpc_cidr" {
description = "CIDR block of the VPC"
value = aws_vpc.main.cidr_block
}
output "public_subnet_ids" {
description = "List of public subnet IDs"
value = aws_subnet.public[*].id
}
output "private_subnet_ids" {
description = "List of private subnet IDs"
value = aws_subnet.private[*].id
}terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
}
}# Root module composes multiple child modules
module "vpc" {
source = "./modules/vpc"
vpc_cidr = "10.0.0.0/16"
environment = "prod"
}
module "database" {
source = "./modules/rds"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
engine = "postgresql"
}
module "app" {
source = "./modules/ecs-service"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
db_endpoint = module.database.endpoint
desired_count = 3
}Each module handles one concern. The root module wires them together.
One module = one logical component. A VPC module, a database module, a service module — not an "everything" module.
variable "instance_type" {
type = string
validation {
condition = can(regex("^t[23]\\.", var.instance_type))
error_message = "Only t2 and t3 instance types allowed."
}
}# Good
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0"
}
# Bad — no version pin
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
}Every module should have:
main.tf — resourcesvariables.tf — all inputsoutputs.tf — all outputsversions.tf — provider requirementsREADME.md — usage examplesModules should inherit the provider from the calling module:
# Bad — provider config in module
provider "aws" {
region = "us-east-1"
}
# Good — module inherits provider from caller
# (no provider block in the module)variable "vpc_cidr" {
description = "CIDR block for the VPC (e.g., 10.0.0.0/16)"
type = string
}
output "vpc_id" {
description = "ID of the created VPC"
value = aws_vpc.main.id
}variable "tags" {
description = "Common tags for all resources"
type = map(string)
default = {}
}
# In resources:
tags = merge(var.tags, { Name = "..." })Error: Module not installedRun terraform init to download modules.
The module doesn't expose that output. Check outputs.tf in the module.
Modules can't have circular dependencies. Restructure so data flows one direction.
Learn by doing with interactive courses on CopyPasteLearn:
A Terraform module is just a directory with .tf files. Keep modules small (one component each), validate inputs, version-pin registry sources, expose useful outputs, and document everything with descriptions. Use module composition to build complex infrastructure from simple, tested building blocks.
Install and run Terraform on Ubuntu 26.04 LTS Resolute Raccoon. Covers sudo-rs as default, APT 3.2 rollback, Kernel 7.0, Wayland-only, ROCm, and building...
Set up OCI Load Balancer with Terraform — backend sets, listeners, SSL certificates, and health checks. Step-by-step guide with code examples and best practi...
Configure OCI Object Storage buckets with Terraform — lifecycle policies, pre-authenticated requests, and replication. Step-by-step guide with code examples ...
Deploy Oracle Autonomous Database with Terraform — ATP and ADW instances, wallets, and private endpoints. Step-by-step guide with code examples and best prac...