Terraform Modules: A Practical Guide
Tags: terraform, cloud, aws
Published: 2023-12-18 16:33:33

Among all the concepts that you will need to know about Terraform (providers, states, workspaces, etc.), there are also modules.

**Terraform modules** are self-contained and reusable collections of infrastructure code, allowing users to **encapsulate and share configurations** for specific components, promoting **ease of maintenance** in infrastructure-as-code projects.

They can be used within your company, or shared publicly with the community.

In this article, we will create a module in the simplest way possible by describing the structure and implementing some **writing best practices** and **code validation mechanisms**.

# Simple example
To illustrate the creation of a module, we will take the common case of S3 buckets to be created on AWS.

The division of the module will depend on your needs. Hashicorp does not recommend writing modules for public resource wrappers, because all these modules are already [community contributed](https://github.com/terraform-aws-modules).
Here we take a simple case for the demonstration, also if you want to provide a private module according to your company's standards, it may still make sense to make a wrapper.

We will implement the [structure explained by Hashicorp](https://developer.hashicorp.com/terraform/language/modules/develop), creating only the basic elements that interest us. We also make sure to follow the recommended [naming conventions](https://www.terraform-best-practices.com).

Here is the structure that we are going to put in place, I will describe all these elements.
```bash
├── .pre-commit-config.yaml
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── examples
│   └── s3_bucket_simple.tf
├── main.tf
├── outputs.tf
├── variables.tf
└── versions.tf
```
## Main resources
**main.tf** defines the main resource we create, in our case, the s3 bucket :
```bash
resource "aws_s3_bucket" "this" {
  bucket = var.name
  tags   = var.tags
}

resource "aws_s3_bucket_ownership_controls" "this" {
  bucket = aws_s3_bucket.this.id
  rule {
    object_ownership = "BucketOwnerPreferred"
  }
}

resource "aws_s3_bucket_acl" "this" {
  depends_on = [aws_s3_bucket_ownership_controls.this]
  bucket     = aws_s3_bucket.this.id
  acl        = "private"
}
```

**variables.tf** defines the variables you can pass to the module : 
```bash
variable "name" {
  type        = string
  default     = null
  description = "Name for the bucket. If omitted, Terraform will assign a random, unique name."
}

variable "tags" {
  type        = map(any)
  default     = null
  description = "Details the tags to apply to the bucket"
}
```

**outputs.tf** defines the attributes you can export from the module :
```bash
output "bucket_id" {
  value       = aws_s3_bucket.this.id
  description = "Bucket Name (aka ID)"
}

output "bucket_arn" {
  value       = aws_s3_bucket.this.arn
  description = "Bucket ARN"
}

output "bucket_name" {
  value       = aws_s3_bucket.this.bucket
  description = "Bucket Name"
}
```

and **versions.tf** defines the authorized versions of Terraform and providers to use the module : 
```bash
terraform {
  required_version = ">=1.2"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 4.0, < 5.16.0"
    }
  }
}
```

## Usage example
An example is described in the **examples/s3_bucket_simple.tf** file, you can use it to inspire yourself in using the module :
```bash
module "test_s3" {
  source = "git::https://github.com/jdxlabs/terraform-s3-module.git?ref=0.0.4"
  name   = "${var.workspace}-${var.env}-test-bucket"

  tags = {
    Name = "${var.workspace}-${var.env}-test-bucket"
    Env  = var.env
  }
}
```

## License
For public repositories, I recommend the GPL (General Public License) v3, it provides strong protections for open-source software, ensuring that any modifications or distributions of the code must also be open source and freely accessible to the community.
If you aren't sure of the license you want to attribute to the project, you can go to [choosealicense.com](https://choosealicense.com/).

## Documentation
There are two levels of documentation : 
- **README.md** gives instructions for using the module
- **CONTRIBUTING.md** provides instructions for improving the module itself

## Quality validation for contributions
When people contribute to the project, we use a tool called [pre-commit](https://pre-commit.com), which allows us to improve the code before the changes are shared to the repository.

We use validation scripts, called hooks, notably those from [pre-commit-terraform](https://github.com/antonbabenko/pre-commit-terraform).

**.pre-commit-config.yaml** defines the hooks we want to execute with pre-commit
```bash
repos:
- repo: https://github.com/antonbabenko/pre-commit-terraform
  rev: v1.85.0
  hooks:
    - id: terraform_fmt
    - id: terraform_tflint
    - id: terraform_checkov
    - id: terraform_docs
```

**[Terraform-fmt](https://developer.hashicorp.com/terraform/cli/commands/fmt)** is a command built into Terraform to ensure that code is formatted correctly.

**[tflint](https://github.com/terraform-linters/tflint)** ensures that Terraform code has no typing or forgetting issues.

**[checkov](https://www.checkov.io)** avoids committing AWS resources with security vulnerabilities, it analyzes the deployed resources and indicates what does not comply with the rules of security best practices.

**[terraform-docs](https://terraform-docs.io)** generates the well-formatted documentation in the README.md file.

## Module exposition
The module can come from a registry or through a local / remote reference.

For local reference, you can call it like this : 
```bash
module "s3" {
  source = "./path/to/your/module"
…
```

For remote reference, you have the choice between **https** and **ssh** :
```bash
# https
module "s3" {
  source = "git::https://github.com/jdxlabs/terraform-s3-module.git?ref=0.0.4"
…

# ssh
module "s3" {
  source = "git@github.com:jdxlabs/terraform-s3-module.git?ref=0.0.4"
…
```

You can put the module on the [Terraform Registry](https://registry.terraform.io), but you can very well use it directly without referencing it on this platform.

If you want to reference it to share with the public community, it's quite simple to publish your git repository there. Simply sign up with your Github account, select your repository and click “publish”.

A good practice is to create git tagged releases, in order to point to them during module calls (you can also enter the changelog to follow developments).

# Conclusion
The complete code of the demonstration is available [on github](https://github.com/jdxlabs/terraform-s3-module) and [on the registry](https://registry.terraform.io/modules/jdxlabs/module/s3/latest).

You now have all the keys to create and publish a complete and well-configured module, to share within your company or publicly with the community.