6.3 Modules

A Terraform module allows to reuse resources in multiple places throughout the project. They act as a container to package resource configurations. Much like in standard programming languages, Terraform code can be organized across multiple files and packages instead of having one single file containing all the code. Wrapping code into a module not only allows to reuse it throughout the project, but also in different environments, for example when deploying a dev and a prod infrastructure. Both environments can reuse code from the same module, just with different settings.

A Terraform module is build as a directory containing one or more resource definition files. Basically, when putting all our code in a single directory, we already have a module. This is exactly what we did in our previous examples. However, terraform does not include subdirectories on its own. Subdirectories must be called explicitly using a terraform moduleparameter. The example below references a module located in a ./network subdirectory and passes two parameters to it.

# main.tf
module "network" {
  source = "./networking"
  create_public_ip = true
  environment = "prod"
}

Each module consists of a similar file structure as the root directory. This includes a main.tf where all resources are specified, as well as files for different data sources such as variables.tf and outputs.tf. However, providers are usually configured only in the root module and are not reused in modules. Note that there are different approaches on where to specify the providers. They are either specified in the main.tf or a separate providers.tf. It does not make a difference for Terraform as it does not distinguish between the resource definition files. It is merely a strategy to keep code and project in a clean and consistent structure.

root
│   main.tf
│   variables.tf
│   outputs.tf
│
└── networking
    │   main.tf
    │   variables.tf
    │   outputs.tf

6.3.1 Input Variables

Each module can have multiple Input Variables. Input Variables serve as parameters for a Terraform module so users can customize behavior without editing the source. In the previous example of importing a network module, there have been two input variables specified, create_public_ip and environment. Input variables are usually specified in the variables.tf file.

# variables.tf
variable "instance_name" {
  type = string
  default = "awesome-instance"
  description = "Name of the aws instance to be created"
}

Each variable has a type (e.g. string, map, set, boolen) and may have a default value and description. Any variable that has no default must be supplied with a value when calling the module reference. This means that variables defined at the root module need values assigned to as a requirement so Terraform will not fail. This can be done by different resources, for example

  • a variable’s default value
  • via the command line using the terraform apply -var="variable=value"option
  • via environment variables starting with TF_VAR_; Terraform will check them automatically
  • a .tfvars file where the variable values are specified; Terraform can load variable definitions from these files automatically (please check online resources for further insights)

Variables can be used in expressions using the var.prefix such as shown in below example. We use the resource configuration of the previous example to create an aws_instance but this time its name is provided by an input variable.

# main.tf
resource "aws_instance" "awesome-instance" {
  ami           = "ami-0ddbdea833a8d2f0d"
  instance_type = "t2.micro"
  
  tags = {
    Name = var.instance_name
  }
}

6.3.2 Output Variables

Similar to Input variables, a terraform module has output variables. As their name states, output variables return values of a Terraform module and are denoted in the outputs.tf file as expected. A module’s consumer has no access to any resources or data created within the module itself. However, sometimes a modules attrivutes are needed for another module or resource. Output variables address this issue by exposing a defined subset of the created resources.

The example below defines an output value instance_address containing the IP address of an EC2 instance the we create with a module. Any module that reference this module can use the instance_address value by referencing it via module.module_name.instance_address

# outputs.tf
output "instance_address" {
      value = aws_instance.awesome-instance.private_ip
      description = "Web server's private IP address"
    }

6.3.3 Local Variables

Additionally to Input variables and output variables a module provides the use of local variables. Local values are basically just a convenience feature to assign a shorter name to an expression and work like standard variables. This means theor scope is also limited to the module they are declared in. Using local variables reduces code repetitions which can be especially valuable when dealing with output variables from a module.

# main.tf
locals {
  vpc_id = module.network.vpc_id
}
module "network" {
  source = "./network"
}
module "service1" {
  source = "./service1"
  vpc_id = local.vpc_id
}
module "service2" {
  source = "./service2"
  vpc_id = local.vpc_id
}