· Guide · 2 min read
Scaling Terraform - Module Composition
Reduce the noise in your modules.

Introduction
In the first part of this series, we covered how to structure a GitOps repository and apply Terraform configurations using a consistant, well-structured pattern and a powerful bit of shell scripting.
In this post, we’ll go beyond generic resource creation and focus on how to scale the creation and management of many resources—cleanly and consistently.
In this post, I’ll cover:
Requirements
What are we building?
We’ve been tasked with building an API for a chatbot. The conversations endpoints are defined below.

How are we building it?
We’re going to use Lambda and API Gateway and define each endpoint with its own Lambda. This helps us reduce the risk of impacting other endpoints if one is having a bad time or if we need to apply specific configurations to an endpoint (memory, runtime, cpu, etc).

Setting Defaults in Maps
Example of Untenable Implementation
We’re going to use for_each meta-argument to loop over a map defined in a local value called api_lambda_functions. Where I see teams begin to struggle is managing the confguraitons for multiple resources, passing in every possible key.
You generally don’t want to define each lambda individually within your variable map. You’ll end up with something that looks like this:
resource "aws_lambda_function" "function" {
for_each = local.api_lambda_functions
architectures = each.value["architectures"]
description = each.value["description"]
function_name = each.value["function_name"]
handler = each.value["handler"]
memory_size = each.value["memory_size"]
publish = each.value["publish"]
runtime = each.value["runtime"]
role = var.iam_role
s3_bucket = var.lambda_function_repository_bucket
s3_key = data.aws_s3_object.lambda_s3_object.key
s3_object_version = data.aws_s3_object.lambda_s3_object.version_id
timeout = each.value["timeout"]
...
}locals {
api_lambda_functions = {
### Conversations Endpoints ###
delete_conversation = {
architectures = var.architectures
description = "Delete conversations"
function_name = "delete-conversation"
memory_size = 128
publish = "true"
route_key = "DELETE /conversations/{id}"
runtime = "python3.11"
timeout = 900
}
get_conversations = {
architectures = var.architectures
description = "Get a user's conversations IDs and conversation titles."
function_name = "get-conversations"
memory_size = 128
publish = "true"
route_key = "GET /conversations"
runtime = "python3.11"
timeout = 900
}
get_conversations_id = {
architectures = var.architectures
description = "Get a user's messages from a specific conversation."
function_name = "get-conversations-id"
memory_size = 128
publish = "true"
route_key = "GET /conversations/{id}"
runtime = "python3.11"
timeout = 900
}
patch_conversation = {
architectures = var.architectures
description = "Update a conversation."
function_name = "patch-conversations"
memory_size = 128
publish = "true"
route_key = "PATCH /conversations/{id}"
runtime = "python3.11"
timeout = 900
}
post_conversations_id = {
architectures = var.architectures
description = "Continues a conversation."
function_name = "post-conversations-id"
memory_size = 512
publish = "true"
route_key = "PATCH /conversations/{id}"
runtime = "python3.11"
timeout = 900
}
put_conversations = {
architectures = var.architectures
description = "Creates a new conversation."
function_name = "put-conversations"
memory_size = 512
publish = "true"
route_key = "PUT /conversations"
runtime = "python3.11"
timeout = 900
}
}
}Implementation With Defaults
Using the try function when defining our lambda resources, we can set default values for keys. This allows us to significantly trim the maps in our local values. The only keys we need to pass in are the description and route_key.
resource "aws_lambda_function" "function" {
for_each = local.api_lambda_functions
architectures = try(each.value["architectures"], ["arm64"])
description = each.value["description"]
function_name = "${var.environment_name}-${replace(each.key, "_", "-")}-${data.terraform_remote_state.region.outputs.region.aws_region_shortname}"
handler = "main.${each.key}"
memory_size = try(each.value["memory_size"], 128)
publish = try(each.value["publish"], "true")
runtime = try(each.value["runtime"], "python3.11")
role = try(each.value["role"], aws_iam_role.iam_role["lambda"].arn) #aws_iam_role.iam_role["${each.key}"].arn
s3_bucket = var.lambda_function_repository_bucket
s3_key = data.aws_s3_object.lambda_s3_object["${try(each.value["tier"], "api")}"].key
s3_object_version = data.aws_s3_object.lambda_s3_object["${try(each.value["tier"], "api")}"].version_id
timeout = try(each.value["timeout"], 900)
...
}locals {
api_lambda_functions = {
### Conversations Endpoints ###
delete_conversation = {
description = "Delete a user's conversation and its messages."
route_key = "DELETE /conversations/{id}"
}
get_conversations = {
description = "Get a user's conversations IDs and conversation titles."
route_key = "GET /conversations"
}
get_conversations_id = {
description = "Get a user's messages from a specific conversation."
route_key = "GET /conversations/{id}"
}
patch_conversation = {
description = "Update a conversation."
route_key = "PATCH /conversations/{id}"
}
post_conversations_id = {
description = "Continues a conversation."
route_key = "POST /conversations/{id}"
memory_size = "512"
}
put_conversations = {
description = "Creates a new conversation."
route_key = "PUT /conversations"
memory_size = "512"
}
}
}
