Terraform modules are like functions for your infrastructure, letting you package and reuse configurations.

Let’s say you want to spin up an RDS Aurora cluster. Normally, you’d write a bunch of HCL for the aws_rds_cluster and aws_rds_cluster_instance resources. But if you’re doing this in multiple environments (dev, staging, prod) or for different applications, you’ll quickly find yourself copy-pasting and changing small things.

Here’s what a basic RDS Aurora setup might look like in Terraform without a module:

resource "aws_rds_cluster" "main" {
  cluster_identifier      = "my-app-db"
  engine                  = "aurora-mysql"
  engine_version          = "8.0.mysql_aurora.3.02.0"
  availability_zones      = ["us-east-1a", "us-east-1b", "us-east-1c"]
  database_name           = "appdb"
  master_username         = "admin"
  master_password         = "verysecretpassword" # In reality, use secrets manager or parameter store!
  skip_final_snapshot     = true
  backup_retention_period = 7
  preferred_backup_window = "07:00-09:00"
  vpc_security_group_ids  = ["sg-0123456789abcdef0"]
  db_subnet_group_name    = "my-db-subnet-group"
}

resource "aws_rds_cluster_instance" "writer" {
  identifier         = "my-app-db-writer"
  cluster_identifier = aws_rds_cluster.main.id
  instance_class     = "db.r6g.large"
  engine             = "aurora-mysql"
  engine_version     = "8.0.mysql_aurora.3.02.0"
  publicly_accessible = false
}

resource "aws_rds_cluster_instance" "reader" {
  identifier         = "my-app-db-reader"
  cluster_identifier = aws_rds_cluster.main.id
  instance_class     = "db.r6g.large"
  engine             = "aurora-mysql"
  engine_version     = "8.0.mysql_aurora.3.02.0"
  publicly_accessible = false
}

This works, but it’s verbose. Now, let’s turn this into a module.

Module Structure

You’d typically create a directory for your module, say modules/rds-aurora. Inside this directory, you’d have:

  • main.tf: Contains the resource definitions (aws_rds_cluster, aws_rds_cluster_instance).
  • variables.tf: Defines the input variables for your module (e.g., cluster name, engine version, instance class).
  • outputs.tf: Defines the outputs of your module (e.g., cluster endpoint, ARN).

modules/rds-aurora/main.tf:

resource "aws_rds_cluster" "this" {
  cluster_identifier      = var.cluster_identifier
  engine                  = var.engine
  engine_version          = var.engine_version
  availability_zones      = var.availability_zones
  database_name           = var.database_name
  master_username         = var.master_username
  master_password         = var.master_password
  skip_final_snapshot     = var.skip_final_snapshot
  backup_retention_period = var.backup_retention_period
  preferred_backup_window = var.preferred_backup_window
  vpc_security_group_ids  = var.vpc_security_group_ids
  db_subnet_group_name    = var.db_subnet_group_name
  tags = merge(
    {
      "ManagedBy" = "Terraform"
      "Environment" = var.environment
    },
    var.tags
  )
}

resource "aws_rds_cluster_instance" "this" {
  count              = var.instance_count
  identifier         = "${var.cluster_identifier}-instance-${count.index}"
  cluster_identifier = aws_rds_cluster.this.id
  instance_class     = var.instance_class
  engine             = var.engine
  engine_version     = var.engine_version
  publicly_accessible = false # Keep this false for security
}

modules/rds-aurora/variables.tf:

variable "cluster_identifier" {
  description = "The unique identifier for the RDS cluster."
  type        = string
}

variable "engine" {
  description = "The database engine to use (e.g., aurora-mysql, aurora-postgresql)."
  type        = string
  default     = "aurora-mysql"
}

variable "engine_version" {
  description = "The engine version for the RDS cluster."
  type        = string
  default     = "8.0.mysql_aurora.3.02.0" # Example for Aurora MySQL
}

variable "availability_zones" {
  description = "A list of Availability Zones for the cluster."
  type        = list(string)
  default     = ["us-east-1a", "us-east-1b", "us-east-1c"]
}

variable "database_name" {
  description = "The name of the initial database to create."
  type        = string
  default     = "appdb"
}

variable "master_username" {
  description = "The master username for the database."
  type        = string
}

variable "master_password" {
  description = "The master password for the database. Use a secrets manager for production."
  type        = string
  sensitive   = true # Mark as sensitive to prevent exposure in logs
}

variable "skip_final_snapshot" {
  description = "Determines whether to skip the final snapshot when the cluster is deleted."
  type        = bool
  default     = true
}

variable "backup_retention_period" {
  description = "The number of days to retain backups."
  type        = number
  default     = 7
}

variable "preferred_backup_window" {
  description = "The daily time range during which automated backups are created."
  type        = string
  default     = "07:00-09:00"
}

variable "vpc_security_group_ids" {
  description = "List of VPC security group IDs to associate with the RDS cluster."
  type        = list(string)
}

variable "db_subnet_group_name" {
  description = "The name of the DB subnet group."
  type        = string
}

variable "instance_count" {
  description = "The number of read replicas to create."
  type        = number
  default     = 1 # Default to one writer instance
}

variable "instance_class" {
  description = "The instance class for the RDS instances (e.g., db.r6g.large)."
  type        = string
  default     = "db.r6g.large"
}

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

variable "tags" {
  description = "A map of additional tags to assign to the resources."
  type        = map(string)
  default     = {}
}

modules/rds-aurora/outputs.tf:

output "cluster_id" {
  description = "The RDS cluster identifier."
  value       = aws_rds_cluster.this.id
}

output "cluster_endpoint" {
  description = "The connection endpoint for the RDS cluster."
  value       = aws_rds_cluster.this.endpoint
}

output "cluster_reader_endpoint" {
  description = "The reader endpoint for the RDS cluster."
  value       = aws_rds_cluster.this.reader_endpoint
}

output "cluster_arn" {
  description = "The ARN of the RDS cluster."
  value       = aws_rds_cluster.this.arn
}

Using the Module

Now, in your root Terraform configuration (or another module), you can call this module like so:

main.tf (in your root directory):

provider "aws" {
  region = "us-east-1"
}

# Assume these resources are defined elsewhere or provided as data sources
data "aws_vpc" "main" {
  filter {
    name   = "tag:Name"
    values = ["my-vpc"]
  }
}

data "aws_subnet_ids" "private" {
  vpc_id = data.vpc.main.id
  filter {
    name = "tag:Tier"
    values = ["private"]
  }
}

resource "aws_db_subnet_group" "default" {
  name       = "my-db-subnet-group"
  subnet_ids = data.aws_subnet_ids.private.ids
}

resource "aws_security_group" "rds" {
  name        = "rds-sg"
  description = "Allow DB access"
  vpc_id      = data.vpc.main.id

  ingress {
    from_port   = 3306 # For Aurora MySQL
    to_port     = 3306
    protocol    = "tcp"
    cidr_blocks = ["10.0.0.0/16"] # Example: Allow access from within your VPC
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "rds-sg"
  }
}

module "app_db_dev" {
  source = "./modules/rds-aurora" # Path to your module

  cluster_identifier = "app-db-dev"
  environment        = "dev"
  master_username    = "devadmin"
  master_password    = "devsecret" # Use a more secure method in production
  vpc_security_group_ids  = [aws_security_group.rds.id]
  db_subnet_group_name    = aws_db_subnet_group.default.name
  instance_count     = 1 # One writer, no readers for dev
  engine_version     = "8.0.mysql_aurora.3.02.0"
  tags = {
    Application = "MyApp"
  }
}

module "app_db_prod" {
  source = "./modules/rds-aurora"

  cluster_identifier = "app-db-prod"
  environment        = "prod"
  master_username    = "prodadmin"
  master_password    = "prodverysecret" # Use AWS Secrets Manager!
  vpc_security_group_ids  = [aws_security_group.rds.id]
  db_subnet_group_name    = aws_db_subnet_group.default.name
  instance_count     = 3 # One writer, two readers for prod
  engine_version     = "8.0.mysql_aurora.3.02.0"
  backup_retention_period = 30
  tags = {
    Application = "MyApp"
  }
}

output "app_db_dev_endpoint" {
  value = module.app_db_dev.cluster_endpoint
}

output "app_db_prod_endpoint" {
  value = module.app_db_prod.cluster_endpoint
}

This setup allows you to define your RDS Aurora cluster configuration once in the module and then reuse it by simply providing different variable values for each instance. This is the core power of modules: abstraction and reusability.

The most surprising thing about Terraform modules is how they change your mental model from "managing resources" to "managing reusable infrastructure components." You start thinking about the interfaces (variables and outputs) more than the specific AWS API calls.

The next concept you’ll likely encounter is managing module versions and sourcing modules from remote locations like Terraform Registry or Git repositories.

Want structured learning?

Take the full Rds course →