Any set of Terraform configuration files in a folder is a module.

Root module - a directory where you run apply command.

Reusable module - module that is imported in other modules.

Reusable modules don’t need to define providers block.

Syntax of using the module:

module "<NAME>" {
  source = "<SOURCE>"
 
  [CONFIG ...]
}
  • NAME - an identifier of the resource that you can use in the Terraform configuration to reference to the imported module.
  • SOURCE - path to the module, e.g. modules/services/users.
  • CONFIG - module arguments. Basically all variable of the module.

Whenever you add a module to the Terraform configuration or modify the source parameter of the module, you need to run init command to download/configure the module.

Output of the module is all the output defined in the configuration, and can be accessed with:

module.<MODULE_NAME>.<OUTPUT_NAME>
 
# e.g.
module.users.cognito_user_pool_id

Gotchas

File paths

Inside the module all paths are relative to the working directory. That means, that to point to the local file inside the reusable module, you need to the a path reference, instead of using relative path (it will not work when )

There are a couple path references in Terraform that may come in handy:

  • path.module - returns the filesystem path of the module where the expression is defined - current reusable module.
  • path.root - returns the filesystem path of the root module.
  • path.cwd - returns the filesystem path of the current working directory. In normal use will be the same as path.root, but in some advanced uses of Terraform may be different.

Example:

user_data = templatefile("${path.module}/user-data.sh", {
	server_port = var.server_port
	db_address  = data.terraform_remote_state.db.outputs.address
	db_port     = data.terraform_remote_state.db.outputs.port
})

Inline blocks

It’s better to use separate resources when available, instead of inline blocks. That way it can be easily extendable from the root module.

# BAD
resource "aws_security_group" "alb" {
  name = "${var.cluster_name}-alb"
 
  ingress {
    from_port   = local.http_port
    to_port     = local.http_port
    protocol    = local.tcp_protocol
    cidr_blocks = local.all_ips
  }
 
  egress {
    from_port   = local.any_port
    to_port     = local.any_port
    protocol    = local.any_protocol
    cidr_blocks = local.all_ips
  }
}
# GOOD
resource "aws_security_group" "alb" {
  name = "${var.cluster_name}-alb"
}
 
resource "aws_security_group_rule" "allow_http_inbound" {
  type              = "ingress"
  security_group_id = aws_security_group.alb.id
 
  from_port   = local.http_port
  to_port     = local.http_port
  protocol    = local.tcp_protocol
  cidr_blocks = local.all_ips
}
 
resource "aws_security_group_rule" "allow_all_outbound" {
  type              = "egress"
  security_group_id = aws_security_group.alb.id
 
  from_port   = local.any_port
  to_port     = local.any_port
  protocol    = local.any_protocol
  cidr_blocks = local.all_ips
}