Sub modules
We created this one module for us that creates the infrastructure for use. We can create as many resources as we like in this module and maybe have the whole infrastructure deployment done using a single main.tf
. That’s not always ideal. You would ideally like to categorize the resources you are creating and sometimes would like to control if a set of resources should be created or not. For example, we can split the current resources into two submodules: network and storage. Maybe in some situation we would just want to create the networking part but skip storage part. With just one variable we will be able to control if we want to create resources related to storage.
Split into modules
In our previous example we just two files in module folder but if we want to split the code modules namely network and storage, we will have structure like below:
├── main.tf
├── network
│ ├── main.tf
│ ├── outputs.tf
│ └── variables.tf
├── storage
│ ├── main.tf
│ └── variables.tf
└── variables.tf
Notice how the network and storage folder look like original module folder that has just files as main.tf
and variables.tf
. This is because the original module could also be used as a sub-module for any other module. If it’s not clear then just read through the doc and it should clear by the end of it.
The outermost main.tf
is the file that will be responsible to call the submodules, i.e. network and storage. But it is possible to use these two modules without using outer main.tf
. Any terraform code an reference the network module, pass the variables the network module needs and the module will create resources. Our outer main.tf
will also do the same. Let’s see how.
Restructuring code to create submodules
The main.tf
within the network module will look like this:
resource "aws_vpc" "tf_vpc" {
cidr_block = var.vpc_cidr
}
resource "aws_subnet" "private_1" {
vpc_id = aws_vpc.tf_vpc.id
cidr_block = var.subnet_private_1
availability_zone = "us-east-1a"
tags = {
Name = "${var.unique_id}-private-1"
}
}
resource "aws_subnet" "private_2" {
vpc_id = aws_vpc.tf_vpc.id
cidr_block = var.subnet_private_2
availability_zone = "us-east-1b"
tags = {
Name = "${var.unique_id}-private-2"
}
}
The variables.tf
file within the network module will look like this:
variable "unique_id" {
}
variable "vpc_cidr" {
}
variable "subnet_private_1" {
}
variable "subnet_private_2" {
}
Module outputs
Along with the main.tf
and variables.tf
in the network submodule, we also have another file called outputs.tf
:
output "vpc_id" {
value = aws_vpc.tf_vpc.id
}
output "subnet_ids" {
value = [aws_subnet.private_1.id,aws_subnet.private_2.id]
}
These outputs are available to the caller of the module so that the caller can it to create downstream resources. In our case, the outer main.tf
is calling the network module like this:
module "network" {
source = "./network"
unique_id = var.unique_id
vpc_cidr = var.vpc_cidr
subnet_private_1 = var.subnet_private_1
subnet_private_2 = var.subnet_private_2
}
module "storage" {
source = "./storage"
unique_id = var.unique_id
database_name = var.database_name
database_password = var.database_password
database_username = var.database_username
vpc_id = module.network.vpc_id
sg_allowed_cidr = [var.vpc_cidr]
subnet_group_ids = module.network.subnet_ids
}
terraform {
backend "s3" {
}
}
Notice how the storage module is fetching the value for subnet_group_ids
by referring to module.network.subnet_ids
. The module
word is a static word that tell terraform that we are going to refer to a module next. The network
word is the name of the ‘module’ resource. And then subnet_ids
is an output generated by the module which is present in the outputs.tf
of the network module. Basically, the module reference becomes like a resource that is accepting some variables and generating some outputs. That’s what a ‘module’ is supposed to do, right?
Even though it is not compulsory for a module to have outputs, but you will always find that whoever is calling your module will need some kind of outputs so that they can pass them further down for more resource creation. The storage
module does not have any outputs because there is no resource that is using the database. Imagine deploying the application via terraform. Then we will have to pass the database hostname to the application, right? That’s when you can add outputs to the terraform module.
Here is how the storage
module looks like:
The main.tf
file:
resource "aws_security_group" "allow_db" {
name = "${var.unique_id}-allow_db"
description = "Allow DB access only within VPC"
vpc_id = var.vpc_id
ingress {
description = "TLS from VPC"
from_port = 5432
to_port = 5432
protocol = "tcp"
cidr_blocks = var.sg_allowed_cidr
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
ipv6_cidr_blocks = ["::/0"]
}
tags = {
Name = "allow_this_vpc"
}
}
resource "aws_db_instance" "tf_db" {
allocated_storage = 10
db_name = var.database_name
engine = "postgres"
engine_version = "13"
instance_class = "db.t3.micro"
username = var.database_username
password = var.database_password
parameter_group_name = "default.postgres13"
skip_final_snapshot = true
db_subnet_group_name = aws_db_subnet_group.subnet_group_1.id
vpc_security_group_ids = [ aws_security_group.allow_db.id ]
}
resource "aws_db_subnet_group" "subnet_group_1" {
name = "${var.unique_id}-main"
subnet_ids = var.subnet_group_ids
tags = {
Name = "${var.unique_id}-subnet-group-1"
}
}
The variables.tf
file:
variable "unique_id" {
}
variable "vpc_id" {
}
variable "sg_allowed_cidr" {
type = list(string)
default = ["0.0.0.0/0"]
}
variable "database_name" {
}
variable "database_password" {
}
variable "database_username" {
}
variable "subnet_group_ids" {
type = list(string)
default = []
}
Control submodule installation
You can decide if you want to deploy the storage submodule or not. You can simply add a variable to the outer variables.tf
and then add one line in the outer main.tf
for the user to control if the module should be installed.
variables.tf
:
.
.
variable "storage_enabled" {
type = bool
default = true
}
and in main.tf
module "storage" {
count = var.storage_enabled ? 1 : 0
source = "./storage"
unique_id = var.unique_id
.
.
.
}
This way the user calling the module can control if a set of resources should be created or not.
There is another benefit of using submodules which is creating a set of resources multiple times. In the above example we added the count variable but its value can either be 0 or 1. But it can have any value and the same module will be called multiple times to create multiple set of resources present in the storage module.
Note that having submodules is different than having resources in separate files within the same module. As mentioned in earlier chapters, for terraform it does not matter if your resources, outputs, module calls, are present in the same file or multiple files. Multiple files are created only for humans to have better readability and management.
Conclusion
Use terraform modules when you have a set of resources that the user can choose if they want or not. Controlling each resource creating is difficult in terraform. But if you put them all in one module then enabling and disabling that one module is easier.