Skip to content

Level Up Your Infrastructure Game: Mastering Terraform Modules for Reusable Magic

Tired of cooking up your infrastructure from scratch every single time? Imagine if you had an Instant Pot for your cloud – something that could whip up a perfect server, database, or network setup with just a few ingredients. That's exactly what Terraform modules are: prepackaged recipes for your digital kitchen.

Image

Why Modules? Because Nobody Likes Starting From Zero

Think of modules as your pre-built LEGO sets. Instead of painstakingly assembling every tiny brick for a spaceship, you grab a set that's already got the main body and wings ready to go. In Terraform, these sets are modules, and they help you create reusable infrastructure pieces.

Benefits You'll Actually Care About

  • Speedy Delivery: Instead of waiting hours for your infrastructure to bake, modules give you "instant" results.
  • Consistent Dishes: No more "oops, I forgot the salt" moments. Modules ensure your infrastructure tastes the same every time.
  • Easy Cleanup: Modules keep your code organized, like having all your spices in labeled jars instead of scattered across the counter.

Let's Cook Up a Simple Module: An EC2 Server "Recipe"

We'll make a module that launches a basic server on AWS, like an "Instant Server" recipe.

1. Recipe Repository

The recipe repository name must follow this naming convention

terraform-<aws|gcp>-<module_name>

Terraform module to deploy an AWS EC2 instance, the module name would be

terraform-aws-ec2-instance

2. Create a Recipe File Tree

Copy and paste these commands to create the file tree
mkdir terraform-aws-ec2-instance
cd terraform-aws-ec2-instance
touch main.tf variables.tf outputs.tf
mkdir -p examples/development
cd examples/development
touch 00-providers.tf 01-data.tf 01-locals.tf 09-module.tf 10-outputs.tf
Recipe File Tree
terraform-aws-ec2-instance
├── examples
   └── development
       ├── 00-providers.tf
       ├── 01-data.tf
       ├── 01-locals.tf
       ├── 09-module.tf
       └── 10-outputs.tf
├── main.tf
├── outputs.tf
├── README.md
└── variables.tf
  • main.tf: main file of the module.
  • variables.tf: module's input variables.
  • outputs.tf: module's outputs.
  • examples/development: an example of how to use this module.
  • README.md : This file must be generated by terraform-docs

3. Write the "Recipe."

terraform-aws-ec2-instance/main.tf
1
2
3
4
5
6
7
resource "aws_instance" "this" {
  ami                    = var.ami
  instance_type          = var.instance_type
  subnet_id              = var.subnet_id
  vpc_security_group_ids = var.vpc_security_group_ids
  tags                   = merge(var.tags, { module : "nhamchanvi/terraform-aws-ec2-instance" })
}
terraform-aws-ec2-instance/variables.tf
variable "name" {
  description = "Name to be used on EC2 instance created"
  type        = string
  default     = ""
}

variable "ami" {
  description = "ID of AMI to use for the instance"
  type        = string
  default     = ""
}

variable "instance_type" {
  description = "The type of instance to start"
  type        = string
  default     = "t3.micro"
}

variable "subnet_id" {
  description = "The VPC Subnet ID to launch in"
  type        = string
  default     = null
}

variable "vpc_security_group_ids" {
  description = "A list of security group IDs to associate with"
  type        = list(string)
  default     = null
}

variable "tags" {
  description = "A mapping of tags to assign to the resource"
  type        = map(string)
  default     = {}
}
terraform-aws-ec2-instance/outputs.tf
1
2
3
4
output "public_ip" {
  description = "The public IP address assigned to the instance, if applicable."
  value       = aws_instance.this.public_ip
}

4. Create the Example for the "Recipe."

examples/development/00-providers.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~>4.0"
    }
  }
}

provider "aws" {
}
examples/development/01-data.tf
data "aws_ami" "debian" {
  most_recent = true

  filter {
    name   = "name"
    values = ["debian-11-amd64-*"]
  }

  filter {
    name   = "root-device-type"
    values = ["ebs"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }

  owners = ["136693071363"]
}

data "aws_vpc" "default" {
  default = true
}

data "aws_subnets" "defaults" {
  filter {
    name   = "vpc-id"
    values = [data.aws_vpc.default.id]
  }
}
examples/development/01-locals.tf
1
2
3
4
5
6
7
8
locals {
  scenario = "development"
  tags = {
    secnario   = "module-development"
    terraform  = true
    repository = "https://github.com/nhamchanvi/terraform-aws-ec2-instance"
  }
}
examples/development/09-module.tf
1
2
3
4
5
6
7
8
module "ec2-instance" {
  source        = "../../"
  name          = "training-terrafrom-module"
  ami           = data.aws_ami.debian.id
  instance_type = "t3.micro"
  subnet_id     = data.aws_subnets.defaults.ids[0]
  tags          = local.tags
}
examples/development/10-outputs.tf

5. Test Your "Recipe."

cd examples/development

terraform init
Initializing the backend...
Initializing modules...
- ec2-instance in ../..

Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 4.0"...
- Installing hashicorp/aws v4.67.0...
- Installed hashicorp/aws v4.67.0 (signed by HashiCorp)

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
terraform validate
Success! The configuration is valid.
terraform fmt

terraform plan
data.aws_vpc.default: Reading...
data.aws_ami.debian: Reading...
data.aws_ami.debian: Read complete after 1s [id=ami-00a5b694d38896fa5]
data.aws_vpc.default: Read complete after 2s [id=vpc-054a2a678b91e3149]
data.aws_subnets.defaults: Reading...
data.aws_subnets.defaults: Read complete after 1s [id=eu-west-1]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # module.ec2-instance.aws_instance.this will be created
  + resource "aws_instance" "this" {
      + ami                                  = "ami-00a5b694d38896fa5"
      + arn                                  = (known after apply)
      + associate_public_ip_address          = (known after apply)
      + availability_zone                    = (known after apply)
      + cpu_core_count                       = (known after apply)
      + cpu_threads_per_core                 = (known after apply)
      + disable_api_stop                     = (known after apply)
      + disable_api_termination              = (known after apply)
      + ebs_optimized                        = (known after apply)
      + get_password_data                    = false
      + host_id                              = (known after apply)
      + host_resource_group_arn              = (known after apply)
      + iam_instance_profile                 = (known after apply)
      + id                                   = (known after apply)
      + instance_initiated_shutdown_behavior = (known after apply)
      + instance_state                       = (known after apply)
      + instance_type                        = "t3.micro"
      + ipv6_address_count                   = (known after apply)
      + ipv6_addresses                       = (known after apply)
      + key_name                             = (known after apply)
      + monitoring                           = (known after apply)
      + outpost_arn                          = (known after apply)
      + password_data                        = (known after apply)
      + placement_group                      = (known after apply)
      + placement_partition_number           = (known after apply)
      + primary_network_interface_id         = (known after apply)
      + private_dns                          = (known after apply)
      + private_ip                           = (known after apply)
      + public_dns                           = (known after apply)
      + public_ip                            = (known after apply)
      + secondary_private_ips                = (known after apply)
      + security_groups                      = (known after apply)
      + source_dest_check                    = true
      + subnet_id                            = "subnet-0ed8441022b56cedc"
      + tags                                 = {
          + "module"     = "nhamchanvi/terraform-aws-ec2-instance"
          + "repository" = "https://github.com/nhamchanvi/terraform-aws-ec2-instance"
          + "secnario"   = "module-development"
          + "terraform"  = "true"
        }
      + tags_all                             = {
          + "module"     = "nhamchanvi/terraform-aws-ec2-instance"
          + "repository" = "https://github.com/nhamchanvi/terraform-aws-ec2-instance"
          + "secnario"   = "module-development"
          + "terraform"  = "true"
        }
      + tenancy                              = (known after apply)
      + user_data                            = (known after apply)
      + user_data_base64                     = (known after apply)
      + user_data_replace_on_change          = false
      + vpc_security_group_ids               = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.
terraform apply
data.aws_vpc.default: Reading...
data.aws_ami.debian: Reading...
data.aws_ami.debian: Read complete after 2s [id=ami-00a5b694d38896fa5]
data.aws_vpc.default: Read complete after 3s [id=vpc-054a2a678b91e3149]
data.aws_subnets.defaults: Reading...
data.aws_subnets.defaults: Read complete after 1s [id=eu-west-1]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # module.ec2-instance.aws_instance.this will be created
  + resource "aws_instance" "this" {
      + ami                                  = "ami-00a5b694d38896fa5"
      + arn                                  = (known after apply)
      + associate_public_ip_address          = (known after apply)
      + availability_zone                    = (known after apply)
      + cpu_core_count                       = (known after apply)
      + cpu_threads_per_core                 = (known after apply)
      + disable_api_stop                     = (known after apply)
      + disable_api_termination              = (known after apply)
      + ebs_optimized                        = (known after apply)
      + get_password_data                    = false
      + host_id                              = (known after apply)
      + host_resource_group_arn              = (known after apply)
      + iam_instance_profile                 = (known after apply)
      + id                                   = (known after apply)
      + instance_initiated_shutdown_behavior = (known after apply)
      + instance_state                       = (known after apply)
      + instance_type                        = "t3.micro"
      + ipv6_address_count                   = (known after apply)
      + ipv6_addresses                       = (known after apply)
      + key_name                             = (known after apply)
      + monitoring                           = (known after apply)
      + outpost_arn                          = (known after apply)
      + password_data                        = (known after apply)
      + placement_group                      = (known after apply)
      + placement_partition_number           = (known after apply)
      + primary_network_interface_id         = (known after apply)
      + private_dns                          = (known after apply)
      + private_ip                           = (known after apply)
      + public_dns                           = (known after apply)
      + public_ip                            = (known after apply)
      + secondary_private_ips                = (known after apply)
      + security_groups                      = (known after apply)
      + source_dest_check                    = true
      + subnet_id                            = "subnet-0ed8441022b56cedc"
      + tags                                 = {
          + "module"     = "nhamchanvi/terraform-aws-ec2-instance"
          + "repository" = "https://github.com/nhamchanvi/terraform-aws-ec2-instance"
          + "secnario"   = "module-development"
          + "terraform"  = "true"
        }
      + tags_all                             = {
          + "module"     = "nhamchanvi/terraform-aws-ec2-instance"
          + "repository" = "https://github.com/nhamchanvi/terraform-aws-ec2-instance"
          + "secnario"   = "module-development"
          + "terraform"  = "true"
        }
      + tenancy                              = (known after apply)
      + user_data                            = (known after apply)
      + user_data_base64                     = (known after apply)
      + user_data_replace_on_change          = false
      + vpc_security_group_ids               = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

module.ec2-instance.aws_instance.this: Creating...
module.ec2-instance.aws_instance.this: Still creating... [10s elapsed]
module.ec2-instance.aws_instance.this: Creation complete after 19s [id=i-04d957d4f303de62d]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
terraform destroy
data.aws_vpc.default: Reading...
data.aws_ami.debian: Reading...
data.aws_ami.debian: Read complete after 1s [id=ami-00a5b694d38896fa5]
data.aws_vpc.default: Read complete after 2s [id=vpc-054a2a678b91e3149]
data.aws_subnets.defaults: Reading...
data.aws_subnets.defaults: Read complete after 0s [id=eu-west-1]
module.ec2-instance.aws_instance.this: Refreshing state... [id=i-04d957d4f303de62d]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  - destroy

Terraform will perform the following actions:

  # module.ec2-instance.aws_instance.this will be destroyed
  - resource "aws_instance" "this" {
      - ami                                  = "ami-00a5b694d38896fa5" -> null
      - arn                                  = "arn:aws:ec2:eu-west-1:842445166689:instance/i-04d957d4f303de62d" -> null
      - associate_public_ip_address          = true -> null
      - availability_zone                    = "eu-west-1b" -> null
      - cpu_core_count                       = 1 -> null
      - cpu_threads_per_core                 = 2 -> null
      - disable_api_stop                     = false -> null
      - disable_api_termination              = false -> null
      - ebs_optimized                        = false -> null
      - get_password_data                    = false -> null
      - hibernation                          = false -> null
      - id                                   = "i-04d957d4f303de62d" -> null
      - instance_initiated_shutdown_behavior = "stop" -> null
      - instance_state                       = "running" -> null
      - instance_type                        = "t3.micro" -> null
      - ipv6_address_count                   = 0 -> null
      - ipv6_addresses                       = [] -> null
      - monitoring                           = false -> null
      - placement_partition_number           = 0 -> null
      - primary_network_interface_id         = "eni-0479a48d34b61566d" -> null
      - private_dns                          = "ip-172-31-3-184.eu-west-1.compute.internal" -> null
      - private_ip                           = "172.31.3.184" -> null
      - public_dns                           = "ec2-54-74-231-76.eu-west-1.compute.amazonaws.com" -> null
      - public_ip                            = "54.74.231.76" -> null
      - secondary_private_ips                = [] -> null
      - security_groups                      = [
          - "default",
        ] -> null
      - source_dest_check                    = true -> null
      - subnet_id                            = "subnet-0ed8441022b56cedc" -> null
      - tags                                 = {
          - "module"     = "nhamchanvi/terraform-aws-ec2-instance"
          - "repository" = "https://github.com/nhamchanvi/terraform-aws-ec2-instance"
          - "secnario"   = "module-development"
          - "terraform"  = "true"
        } -> null
      - tags_all                             = {
          - "module"     = "nhamchanvi/terraform-aws-ec2-instance"
          - "repository" = "https://github.com/nhamchanvi/terraform-aws-ec2-instance"
          - "secnario"   = "module-development"
          - "terraform"  = "true"
        } -> null
      - tenancy                              = "default" -> null
      - user_data_replace_on_change          = false -> null
      - vpc_security_group_ids               = [
          - "sg-0078b23a0d63568f7",
        ] -> null

      - capacity_reservation_specification {
          - capacity_reservation_preference = "open" -> null
        }

      - cpu_options {
          - core_count       = 1 -> null
          - threads_per_core = 2 -> null
        }

      - credit_specification {
          - cpu_credits = "unlimited" -> null
        }

      - enclave_options {
          - enabled = false -> null
        }

      - maintenance_options {
          - auto_recovery = "default" -> null
        }

      - metadata_options {
          - http_endpoint               = "enabled" -> null
          - http_put_response_hop_limit = 1 -> null
          - http_tokens                 = "optional" -> null
          - instance_metadata_tags      = "disabled" -> null
        }

      - private_dns_name_options {
          - enable_resource_name_dns_a_record    = false -> null
          - enable_resource_name_dns_aaaa_record = false -> null
          - hostname_type                        = "ip-name" -> null
        }

      - root_block_device {
          - delete_on_termination = true -> null
          - device_name           = "/dev/xvda" -> null
          - encrypted             = false -> null
          - iops                  = 100 -> null
          - tags                  = {} -> null
          - throughput            = 0 -> null
          - volume_id             = "vol-0a24924083e4afd34" -> null
          - volume_size           = 8 -> null
          - volume_type           = "gp2" -> null
        }
    }

Plan: 0 to add, 0 to change, 1 to destroy.

Do you really want to destroy all resources?
  Terraform will destroy all your managed infrastructure, as shown above.
  There is no undo. Only 'yes' will be accepted to confirm.

  Enter a value: yes

module.ec2-instance.aws_instance.this: Destroying... [id=i-04d957d4f303de62d]
module.ec2-instance.aws_instance.this: Still destroying... [id=i-04d957d4f303de62d, 10s elapsed]
module.ec2-instance.aws_instance.this: Still destroying... [id=i-04d957d4f303de62d, 20s elapsed]
module.ec2-instance.aws_instance.this: Still destroying... [id=i-04d957d4f303de62d, 30s elapsed]
module.ec2-instance.aws_instance.this: Still destroying... [id=i-04d957d4f303de62d, 40s elapsed]
module.ec2-instance.aws_instance.this: Still destroying... [id=i-04d957d4f303de62d, 50s elapsed]
module.ec2-instance.aws_instance.this: Destruction complete after 54s

Destroy complete! Resources: 1 destroyed.

6. Create README.md with terraform-docs

brew install terraform-docs
==> Downloading https://formulae.brew.sh/api/formula.jws.json
==> Downloading https://ghcr.io/v2/homebrew/core/terraform-docs/manifests/0.19.0
################################################################################################################################# 100.0%
==> Fetching terraform-docs
==> Downloading https://ghcr.io/v2/homebrew/core/terraform-docs/blobs/sha256:73916d978b414105ca9a9d3d264e064b91e90cd43180a6f391372275fa19d563
################################################################################################################################# 100.0%
==> Pouring terraform-docs--0.19.0.x86_64_linux.bottle.tar.gz
==> Downloading https://formulae.brew.sh/api/cask.jws.json
==> Caveats
zsh completions have been installed to:
  /home/linuxbrew/.linuxbrew/share/zsh/site-functions
==> Summary
🍺  /home/linuxbrew/.linuxbrew/Cellar/terraform-docs/0.19.0: 8 files, 23.2MB
==> Running `brew cleanup terraform-docs`...
Disable this behaviour by setting HOMEBREW_NO_INSTALL_CLEANUP.
Hide these hints with HOMEBREW_NO_ENV_HINTS (see `man brew`).
terraform-docs markdown table . > README.md

cat README.md
## Requirements

No requirements.

## Providers

| Name | Version |
|------|---------|
| <a name="provider_aws"></a> [aws](#provider\_aws) | n/a |

## Modules

No modules.

## Resources

| Name | Type |
|------|------|
| [aws_instance.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/instance) | resource |

## Inputs

| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| <a name="input_ami"></a> [ami](#input\_ami) | ID of AMI to use for the instance | `string` | `""` | no |
| <a name="input_instance_type"></a> [instance\_type](#input\_instance\_type) | The type of instance to start | `string` | `"t3.micro"` | no |
| <a name="input_name"></a> [name](#input\_name) | Name to be used on EC2 instance created | `string` | `""` | no |
| <a name="input_subnet_id"></a> [subnet\_id](#input\_subnet\_id) | The VPC Subnet ID to launch in | `string` | `null` | no |
| <a name="input_tags"></a> [tags](#input\_tags) | A mapping of tags to assign to the resource | `map(string)` | `{}` | no |
| <a name="input_vpc_security_group_ids"></a> [vpc\_security\_group\_ids](#input\_vpc\_security\_group\_ids) | A list of security group IDs to associate with | `list(string)` | `null` | no |

## Outputs

| Name | Description |
|------|-------------|
| <a name="output_public_ip"></a> [public\_ip](#output\_public\_ip) | The public IP address assigned to the instance, if applicable. |

Why This Is Like an Instant Pot

  • module "ec2_instance": Says, "Hey Terraform, use my instant server recipe."
  • source = "../../": Tells it where to find the recipe.
  • We give it the ingredients (ami, instance_type, name).
  • The module cooks up the server, and we get the address.

Simple Cooking Tips

  • Make recipes that do one thing well (like "Instant Pot Chicken").
  • Write notes on your recipes (comments) so you remember what's what.
  • Share your recipes with your team!

Conclusion

In essence, Terraform modules are your infrastructure's secret weapon, transforming complex deployments into streamlined, repeatable processes. By embracing this "Instant Pot" approach to infrastructure as code, you'll not only save valuable time and resources but also ensure consistency and collaboration across your team. So, ditch the tedious manual labor and start building with the reusable magic of Terraform modules, unlocking a new level of efficiency and simplicity in your cloud deployments.

Comments