Terraform modules are used to simplify the process of organizing and reusing terraform code. Essentially, a module is a container for multiple resources that are logically connected and used together. This blog post explains the steps to create a Terraform module for managing AWS S3 buckets.
When to use mods?
- You need to reuse infrastructure code in different projects
- You want to keep your Terraform code organized and maintainable
- You want to encapsulate the logic of a set of resources or a single resource
- You want to minimize duplication
- Your goal is to follow best practices
Step 1: Set up the module structure
I always try to follow this structure, especially if the mod is centered around a core service (like AWS S3 in this example):
module/
|-- main.tf # Core resource definitions
|-- variables.tf # Input variables
|-- outputs.tf # Output values
|-- providers.tf # Required providers
Step 2: Define resources in main.tf
- this main.tf The file defines the core resources of the module. please notes I’ve practiced putting all mod related resources into main.tf files, because I tend to keep mods as short as possible, but I’ve seen a lot of mods that require multiple resources, and then each file is for a specific resource.
- For our use case, the S3 bucket example, main.tf Will include:
- Create an S3 bucket with optional lifecycle rules to ignore specific changes.
- Public access blocks ensure that buckets are protected from public access.
- Version control for data recovery.
- Server-side encryption, configure encryption using KMS keys.
- The bucket policy is optional and only applies under the following circumstances Enable_s3_bucket_policy set to real.
This is the content main.tf:
resource "aws_s3_bucket" "this" {
bucket = var.bucket_name
lifecycle {
ignore_changes = [
tags_all,
]
}
}
resource "aws_s3_bucket_public_access_block" "this" {
bucket = aws_s3_bucket.this.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_s3_bucket_versioning" "this" {
bucket = aws_s3_bucket.this.id
versioning_configuration {
status = var.bucket_versioning
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "this" {
bucket = aws_s3_bucket.this.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
kms_master_key_id = var.kms_arn
}
}
}
resource "aws_s3_bucket_policy" "this" {
count = var.enable_s3_bucket_policy ? 1 : 0
bucket = aws_s3_bucket.this.id
policy = var.s3_bucket_policy
}
It’s worth noting that you should determine which parts of your mod need to be configurable and use Terraform variables for those parts. For properties that are consistent across all module calls, you can define them directly within the module.
For example, the resource used to manage S3 bucket-level public access is set to real Because I know all my buckets need to be blocked from public access otherwise you can set a variable here and set it to real or Wrong Depends on what you need.
Additionally, if you are not 100% sure whether you want to disable S3 versioning for a specific bucket, you can use the defaults: real Define a variable and then override the value using in the module call Wrong.
There are use cases when you need more flexibility in setting up a specific resource, such as aws_s3_bucket_policy in this case. this Count This makes sense since we want the bucket policy to be Elective Based on variable values. Without it, you won’t be able to skip strategies if the resource is already defined in the mod.
Step 3: Enter variables (variables.tf)
this variables.tf File defines the input to the module. This allows users to customize the behavior of the module without modifying its internal logic. For this module, we have defined the following variables:
variable "bucket_name" {
description = "The name of the bucket"
type = string
}
variable "bucket_versioning" {
description = "Enable or disable bucket versioning"
type = bool
default = true
}
variable "enable_s3_bucket_policy" {
description = "Whether to enable the S3 bucket policy"
type = bool
default = false
}
variable "s3_bucket_policy" {
description = "value of the s3 bucket policy"
type = string
default = ""
}
variable "kms_arn" {
description = "The ARN of the KMS key to use for server-side encryption"
type = string
}
Step 4: Required providers (providers.tf)
terraform {
required_version = "~> 1.9.7"
required_providers {
aws = {
source = "aws"
version = ">= 4.37"
}
}
}
Step 5: Output values (outputs.tf)
this output.tf File defines the values that the module will expose. This allows users to retrieve important information such as the bucket’s name and ARN.
output "bucket_name" {
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
}
Step 6: Use the mod
After building the mod, you can use it in the root configuration. Here’s an example of how to call this module:
module "test_bucket" {
source = "../modules/s3"
bucket_name = "${local.env}-test-${random_password.random_hash.result}"
kms_arn = aws_kms_key.test_key.arn
}
Since we have the option to build a bucket strategy as needed, the module call might look like this:
module "test_bucket" {
source = "../modules/s3"
bucket_name = "${local.env}-test-${random_password.random_hash.result}"
kms_arn = aws_kms_key.this.arn
enable_s3_bucket_policy = true
s3_bucket_policy = data.aws_iam_policy_document.allow_test_bucket_access.json
}
notes Double check the mod’s source code or the path it is located in, but always try to follow the practice of creating directories module and put all mods there.
in conclusion
Building a Terraform module involves organizing your code into a well-structured format and defining reusable components. In this example, we build a complete module for managing S3 buckets, including advanced features such as version control, encryption, and policies. By following these steps, you can build powerful and reusable modules to meet your infrastructure needs.