Guide to creating reusable local Terraform modules and composing them to provision AWS VPC subnet and EC2 resources while managing inputs outputs and module wiring
Welcome. In this lesson you’ll learn how to create local Terraform modules and compose them in a parent configuration to provision AWS resources. The focus is on module structure, inputs/outputs, and wiring outputs from one module into another so you can reuse infrastructure code across projects and environments.
Create a top-level Terraform directory (this is the parent module). Inside it add common files and a modules subdirectory with three child modules.
Location
Recommended files
Top-level (parent)
main.tf, variables.tf, outputs.tf, providers.tf
modules/vpc
main.tf, variables.tf, outputs.tf
modules/subnet
main.tf, variables.tf, outputs.tf
modules/ec2
main.tf, variables.tf, outputs.tf
Below are cleaned-up module implementations and the parent configuration examples. Keep these modules focused and parameterized so they are reusable across accounts and environments.
variable "vpc_name" { description = "Name of the VPC" type = string default = "my-cool-vpc-for-modules"}variable "vpc_cidr" { description = "CIDR block for the VPC" type = string default = "10.0.0.0/16"}
modules/vpc/outputs.tf
output "vpc_id" { description = "The ID of the VPC" value = aws_vpc.vpc.id}
This module provisions a subnet and the supporting network resources: an Internet Gateway, a route table, and a route table association. It accepts a vpc_id input and returns the subnet_id.modules/subnet/main.tf
variable "vpc_id" { description = "The ID of the VPC" type = string}variable "subnet_cidr" { description = "CIDR block for the subnet" type = string default = "10.0.1.0/24"}variable "subnet_name" { description = "Name of the subnet" type = string default = "demo-subnet"}variable "availability_zone" { description = "Availability zone for the subnet" type = string default = "us-east-1a"}
modules/subnet/outputs.tf
output "subnet_id" { description = "The ID of the subnet" value = aws_subnet.subnet.id}
This module creates a security group and an EC2 instance. Inputs include VPC and subnet IDs plus AMI, instance type, and an instance name. Outputs include the instance ID and public IP.modules/ec2/main.tf
variable "vpc_id" { description = "The ID of the VPC" type = string}variable "subnet_id" { description = "The ID of the subnet" type = string}variable "ami_id" { description = "The AMI ID to use for the instance" type = string default = "ami-0c55b159cbfafe1f0" # example Amazon Linux 2 AMI in us-east-1}variable "instance_type" { description = "The type of instance to start" type = string default = "t2.micro"}variable "instance_name" { description = "Name of the EC2 instance" type = string default = "my-instance"}
modules/ec2/outputs.tf
output "instance_id" { description = "The ID of the instance" value = aws_instance.instance.id}output "public_ip" { description = "The public IP address of the instance" value = aws_instance.instance.public_ip}
After you add or change modules, follow this basic workflow.
Initialize the working directory (downloads providers and registers modules)
$ terraform initInitializing the backend...Initializing modules...- vpc in modules/vpc- subnet_module in modules/subnet- prod-workload in modules/ec2Initializing provider plugins...- Finding latest version of hashicorp/aws...- Installing hashicorp/aws v5.89.0...
Format your files
$ terraform fmt
Create and review a plan, then apply
$ terraform planPlan: X to add, 0 to change, 0 to destroy.
Example of a planned resource created by a child module:
# module.vpc.aws_vpc.vpc will be createdresource "aws_vpc" "vpc" { + arn = (known after apply) + cidr_block = "10.0.0.0/16" + default_network_acl_id = (known after apply) + default_route_table_id = (known after apply) + default_security_group_id = (known after apply) ...}
Use module outputs to pass information between modules (for example: module.vpc.vpc_id -> module.subnet_module.vpc_id). Keep modules small, well-documented, and parameterized so they can be reused across environments.
Running terraform apply will create resources in your cloud account and may incur charges. Always review the plan before applying and destroy resources when they are no longer needed.
Use descriptive variable names and include description in each variables.tf.
Prefer explicit module inputs over relying on implicit defaults in a parent configuration.
You can call a module multiple times with different arguments, or use for_each to create multiple instances of a module.
Split responsibilities logically (networking, compute, database) to simplify testing and reuse.
Consider versioning modules if you extract them to a shared registry.
You created a local module structure (vpc, subnet, ec2), implemented resources along with variables and outputs, and wired module outputs into parent module inputs.
This modular approach reduces duplication and makes it easy to create multiple similar environments by calling the same module with different inputs.