Introduction
Terraform has become the industry standard for provisioning and managing infrastructure as code (IaC). While it’s easy to get started with writing Terraform configurations, building infrastructure that is scalable, maintainable, and robust requires a more disciplined approach. This is where Terraform modules come in.
This post will take a deep dive into creating reusable and testable Terraform modules. We will explore the principles of module design, best practices for creating a clean module structure, and how to implement automated testing to ensure your modules are reliable and production-ready.
Why Use Terraform Modules?
Terraform modules are self-contained packages of Terraform configurations that are managed as a group. They are the building blocks of Terraform and provide a number of benefits:
- Reusability: Modules can be reused across multiple projects and environments, reducing code duplication and ensuring consistency.
- Maintainability: Modules encapsulate complexity, making your infrastructure code easier to understand, manage, and update.
- Scalability: Modules allow you to build complex infrastructure from smaller, more manageable components.
- Testability: Modules can be tested in isolation, making it easier to catch errors and prevent regressions.
Designing a Reusable Module
A well-designed module is easy to understand, use, and extend. Here are some best practices for designing reusable modules:
- Keep it focused: A module should have a single, well-defined purpose. For example, a module for creating a virtual machine should not also be responsible for creating a database.
- Use a clear and consistent naming convention: The names of your variables, outputs, and resources should be clear, concise, and consistent.
- Provide sensible defaults: Your module should provide sensible default values for all variables, making it easy to use out of the box.
- Document your module: Your module should include a
README.md
file that explains what the module does, how to use it, and what variables and outputs it has.
Module Structure
A typical Terraform module has the following structure:
.
├── main.tf
├── variables.tf
├── outputs.tf
└── README.md
main.tf
: This file contains the main set of configurations for your module.variables.tf
: This file contains the variable definitions for your module.outputs.tf
: This file contains the output definitions for your module.README.md
: This file contains the documentation for your module.
Example: A Reusable AWS S3 Bucket Module
Let’s create a reusable module for creating an AWS S3 bucket.
main.tf
resource "aws_s3_bucket" "this" {
bucket = var.bucket_name
acl = var.acl
versioning {
enabled = var.versioning_enabled
}
tags = var.tags
}
variables.tf
variable "bucket_name" {
description = "The name of the S3 bucket."
type = string
}
variable "acl" {
description = "The canned ACL to apply to the bucket."
type = string
default = "private"
}
variable "versioning_enabled" {
description = "A boolean that indicates if versioning should be enabled for the bucket."
type = bool
default = true
}
variable "tags" {
description = "A map of tags to assign to the bucket."
type = map(string)
default = {}
}
outputs.tf
output "bucket_id" {
description = "The name of the bucket."
value = aws_s3_bucket.this.id
}
output "bucket_arn" {
description = "The ARN of the bucket."
value = aws_s3_bucket.this.arn
}
Testing Your Terraform Modules
Testing is a critical part of building reliable infrastructure. There are several tools available for testing Terraform modules, including:
terraform validate
: This command checks the syntax of your Terraform code.terraform plan
: This command creates an execution plan, which shows you what changes Terraform will make to your infrastructure.- Terratest: A Go library that provides patterns and helpers for testing Terraform code.
- kitchen-terraform: A Test Kitchen plugin for testing Terraform code.
Using Terratest for Integration Testing
Terratest is a popular choice for testing Terraform modules. It allows you to write integration tests in Go that provision real infrastructure and then verify that it is configured correctly.
Here is an example of a Terratest test for our S3 bucket module:
package test
import (
"testing"
"github.com/gruntwork-io/terratest/modules/aws"
"github.com/gruntwork-io/terratest/modules/terraform"
"github.com/stretchr/testify/assert"
)
func TestTerraformAwsS3Example(t *testing.T) {
t.Parallel()
// Specify the region where the S3 bucket will be created
awsRegion := "us-west-2"
// Construct the terraform options with default retryable errors to handle the most common
// retryable errors in terraform testing.
terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
// The path to where our Terraform code is located
TerraformDir: "./",
// Variables to pass to our Terraform code using -var options
Vars: map[string]interface{}{
"bucket_name": "terratest-s3-bucket-example",
},
// Environment variables to set when running Terraform
EnvVars: map[string]string{
"AWS_DEFAULT_REGION": awsRegion,
},
})
// At the end of the test, run `terraform destroy` to clean up any resources that were created
defer terraform.Destroy(t, terraformOptions)
// This will run `terraform init` and `terraform apply` and fail the test if there are any errors
terraform.InitAndApply(t, terraformOptions)
// Run `terraform output` to get the value of an output variable
bucketID := terraform.Output(t, terraformOptions, "bucket_id")
// Verify that the S3 bucket exists
aws.AssertS3BucketExists(t, awsRegion, bucketID)
}
Conclusion
Creating reusable and testable Terraform modules is a critical part of building scalable, maintainable, and robust infrastructure as code. By following the best practices outlined in this post, you can create modules that are easy to use, extend, and test.
Comments