Terraform modules

How to work with Terraform modules

Learn about Terraform reusable code from inside your project or in an external resource

Andre Lopes
Level Up Coding
Published in
5 min readDec 26, 2023

--

Image by Carola68 Die Welt ist bunt…… from Pixabay

Hey earthlings!🌍

If you’re diving into the world of infrastructure as code using Terraform, you’ve probably encountered the need for efficient, reusable, and maintainable code. One powerful solution to this challenge is the use of Terraform modules.

In this article, we’ll explore the concept of Terraform modules and guide you through building a practical example — an AWS Simple Queue Service (SQS) module. Whether you’re a Terraform enthusiast or just getting started, having a modular approach can significantly enhance your infrastructure provisioning process.

But first, make sure you’ve got your favorite text editor (I’ll be using Visual Studio Code) and a GitHub account ready. Let’s embark on a journey to create modular, reusable, and shareable Terraform code for your infrastructure needs. 🚀

Requirements

  • Your favorite text editor — I’ll be using Visual Studio Code
  • GitHub account

Terraform modules

Terraform modules are blocks of reusable code for building resources with Terraform.

Think of it like a function that you extract to reuse in your code, instead of re-writing it everywhere.

With this comes some benefits:

  • Reusability — You won’t need to write the same code over to create a similar resource
  • Reduced duplicated code
  • Uniformity across your infrastructure — this means that all resources created from that module will have the same structure

Building your module

Let’s build an AWS SQS module.

The code might be specific to a resource, but the concepts apply to all.

Initialize a root folder and create a sqs folder with these files: main.tf, variables.tf, outputs.tf, datasources.tf.

  • main.tf — Here is where all the main code of our module will reside.
    If you have a complex module, with many resources, you can separate these into multiple named files. E.g: lambda.tf, sqs.tf
  • variables.tf — Defines the external variables our module accepts
  • outputs.tf — Defines the outputs our module will produce
  • datasources.tf — Defines and data structure needed for our module

Inside the variables.tf, add the following code:

variable "name" {
type = string
description = "The Lambda function language runtime"
}

variable "deadletter_queue" {
type = bool
description = "If the SQS queue requires a deadletter queue"
default = false
}

variable "region" {
description = "Default region of your resources"
type = string
}

variable "account_id" {
description = "The ID of the default AWS account"
type = string
}

variable "allowed_services" {
type = list(string)
description = "The services that are allowed to send messages to this SQS queue"
}

In the datasources.tf , add:

data "aws_iam_policy_document" "sqs-queue-policy" {
policy_id = "arn:aws:sqs:${var.region}:${var.account_id}:${var.name}/SQSDefaultPolicy"

statement {
sid = "${var.name}-allow-send-messages"
effect = "Allow"

principals {
type = "Service"
identifiers = ["*"]
}

actions = [
"SQS:SendMessage",
]

resources = [
"arn:aws:sqs:${var.region}:${var.account_id}:${var.name}",
]

condition {
test = "ArnEquals"
variable = "aws:SourceArn"

values = var.allowed_services
}
}
}

In this file, we are defining policies for services to be allowed to push messages to the SQS queue.

Now, for our main.tf :

resource "aws_sqs_queue" "queue" {
name = "${var.name}-queue"
policy = data.aws_iam_policy_document.sqs-queue-policy.json
}

resource "aws_sqs_queue" "deadletter" {
count = var.deadletter_queue ? 1 : 0
name = "${var.name}-queue-dlq"
redrive_allow_policy = jsonencode({
redrivePermission = "byQueue",
sourceQueueArns = [aws_sqs_queue.queue.arn]
})
}

And for last, let’s define the outputs of our module in outputs.tf :

output "queue_arn" {
value = aws_sqs_queue.queue.arn
}

output "queue_name" {
value = aws_sqs_queue.queue.name
}

output "dlq_arn" {
value = var.deadletter_queue ? aws_sqs_queue.deadletter[0].arn : null
}

output "dlq_name" {
value = var.deadletter_queue ? aws_sqs_queue.deadletter[0].name : null
}

Great! We now have our module ready to use.

Using your module

We have multiple ways of using a custom module.

  • Internal reference— When you define and use your module in the same repository.
  • External reference — When your module is defined and hosted in an external repository. E.g: GitHub

Internal source

To use in your internal project it is quite simple. The only thing you need to do is reference directly the folder where your module is defined in a module resource.

Let’s suppose you add the SQS module we created in a ./modules/sqs folder from the folder you define your main Terraform code, then you just need to reference it like:

module "my_sqs_queue" {
source = "./modules/sqs"
name = "my-sqs"
deadletter_queue = true
allowed_services = ["ARN_TO_YOUR_SERVICE"]
region = "YOUR_REGION"
account_id = "YOUR_AWS_ACCOUNT_ID"
}

Once you run your terraform plan, you can see the module being used.

External source

Now, let’s say you want to share this module, you can host it in a GitHub repository and reference it in your code.

module "my_sqs_queue" {
source = "github.com/alopes2/terraform-modules/sqs"
name = "my-queue"
region = "YOUR_REGION"
account_id = "YOUR_AWS_ACCOUNT_ID"
}

For more information on module sourcing, you can check the documentation here.

Versioning your public module

Now, let’s say the repository that hosts the module has a breaking change, but your application doesn’t require it and does not want this breaking change to affect your infrastructure.

If you are referencing the source without any ref, your module is referencing directly the latest commit in the default branch (usually main or master).

To reference a specific point in the repository history, you can make use of tags:

module "my_sqs_queue" {
source = "github.com/alopes2/terraform-modules/sqs?ref=v1"
name = "my-queue"
region = "YOUR_REGION"
account_id = "YOUR_AWS_ACCOUNT_ID"
}

Alternatively, you can also refer directly to the commit you want to:

module "my_sqs_queue" {
source = "github.com/alopes2/terraform-modules/sqs?ref=3f660d839bec8f2e2916016b88480d5a81f4e36e"
name = "my-queue"
region = "YOUR_REGION"
account_id = "YOUR_AWS_ACCOUNT_ID"
}

For more information on versioning, you can check the documentation here.

Conclusion

We got to the end of our journey!

In this story, you learned how to make the best of the powerful features that are Terraform modules to enable the power of reusable and shareable infrastructure code.

We saw how to build a module from scratch and how to use it in both internal and external sources.

Not just that, we also learned how to leverage the powerful feature of versioning by using references to a tag or even a direct commit, allowing us to be resilient and shielded from breaking changes that are not required by our application infrastructure.

Happy coding 💻

For more references on terraform modules, please check this Spacelift article by Stanislaw Szymanski.

--

--