Terraform Module Composition Prompt
Design Terraform modules — input/output contracts, composition, versioning, public vs private registry, when to abstract.
- Target user
- Engineers building reusable Terraform modules
- Difficulty
- Intermediate
- Tools
- Claude, ChatGPT
The prompt
You are a senior Terraform engineer who has built and maintained reusable modules used across many teams — strong inputs, clear outputs, versioned.
I will provide:
- The module purpose
- Current state
- Use case
Your job:
1. **Module design principles**:
- Single responsibility (one logical concern)
- Composable (works with other modules)
- Parameterized (inputs over assumptions)
- Stable outputs (downstream depends)
- Documented
2. **For module structure**:
```
mymodule/
├── main.tf
├── variables.tf
├── outputs.tf
├── versions.tf # provider version requirements
├── README.md
└── examples/
└── basic/
└── main.tf
```
3. **For variables.tf**:
- Document each var (description)
- Specify type
- Default for optional
- Validation
4. **For outputs.tf**:
- Expose what consumers need
- Stable interface
- `sensitive = true` for secrets
5. **For versioning**:
- Tag releases (semver)
- Source with `?ref=v1.2.0`
- Public registry: `registry.terraform.io/<ns>/<name>`
- Private: git/HTTPS/OCI
6. **For composition**:
- Root module wires children
- Outputs of one → inputs of another
- Loose coupling via data sources
7. **For abstraction level**:
- Too thin: just a passthrough; adds nothing
- Too thick: hides too much, hard to override
- Sweet spot: encapsulates pattern + reasonable defaults
8. **For breaking changes**:
- Major version bump
- Migration guide
- Avoid in module versions <1.0
Mark DESTRUCTIVE: changing required variables without migration, removing outputs that callers depend on, module versioning without backward compat plan.
---
Module purpose: [DESCRIBE]
Current state: [DESCRIBE]
Use case: [DESCRIBE]
Why this prompt works
Module design enables reuse. This prompt walks patterns.
How to use it
- Single responsibility.
- Clear inputs/outputs.
- Pin versions.
- Test before publishing.
Patterns
Module structure
modules/network/
├── main.tf
├── variables.tf
├── outputs.tf
├── versions.tf
├── README.md
└── examples/
├── simple/
│ └── main.tf
└── multi-az/
└── main.tf
variables.tf
variable "vpc_cidr" {
description = "CIDR block for the VPC"
type = string
validation {
condition = can(cidrhost(var.vpc_cidr, 0))
error_message = "vpc_cidr must be a valid CIDR block."
}
}
variable "az_count" {
description = "Number of availability zones to use"
type = number
default = 2
validation {
condition = var.az_count >= 2 && var.az_count <= 6
error_message = "az_count must be between 2 and 6."
}
}
variable "tags" {
description = "Tags applied to all resources"
type = map(string)
default = {}
}
variable "enable_flow_logs" {
description = "Enable VPC flow logs"
type = bool
default = false
}
outputs.tf (stable contract)
output "vpc_id" {
description = "ID of the VPC"
value = aws_vpc.main.id
}
output "public_subnet_ids" {
description = "List of public subnet IDs"
value = aws_subnet.public[*].id
}
output "private_subnet_ids" {
description = "List of private subnet IDs"
value = aws_subnet.private[*].id
}
output "availability_zones" {
description = "List of availability zones used"
value = data.aws_availability_zones.available.names
}
versions.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0, < 6.0"
}
}
}
README.md template
# network module
VPC with public/private subnets across multiple AZs.
## Usage
\`\`\`hcl
module "network" {
source = "git::https://gitlab.example.com/platform/tf-modules.git//network?ref=v1.2.0"
vpc_cidr = "10.0.0.0/16"
az_count = 3
tags = { Environment = "production" }
}
\`\`\`
## Requirements
| Name | Version |
|---|---|
| terraform | >= 1.5.0 |
| aws | >= 5.0 |
## Inputs
| Name | Type | Default | Required |
|---|---|---|---|
| vpc_cidr | string | n/a | yes |
| az_count | number | 2 | no |
| tags | map(string) | {} | no |
## Outputs
| Name | Description |
|---|---|
| vpc_id | VPC ID |
| public_subnet_ids | Public subnet IDs |
| private_subnet_ids | Private subnet IDs |
See `examples/` for usage patterns.
Composition pattern
# Root module wires children
module "network" {
source = "../modules/network"
vpc_cidr = "10.0.0.0/16"
}
module "compute" {
source = "../modules/compute"
# outputs from network → inputs to compute
vpc_id = module.network.vpc_id
subnet_ids = module.network.private_subnet_ids
instance_type = "t3.medium"
}
module "monitoring" {
source = "../modules/monitoring"
# outputs from compute → inputs to monitoring
instance_ids = module.compute.instance_ids
}
Module versioning (semver)
# Initial release
git tag v0.1.0 -m "Initial"
git push origin v0.1.0
# Patch (no breaking changes)
git tag v0.1.1 -m "Bug fix"
git push origin v0.1.1
# Minor (backward-compat features)
git tag v0.2.0 -m "Add IPv6 support"
git push origin v0.2.0
# Major (breaking change)
git tag v1.0.0 -m "Renamed variables, removed deprecated outputs"
git push origin v1.0.0
Common findings this catches
- Module too thin (just wraps a resource) — collapse.
- Module too fat (hides everything) — split or expose more.
- No README → document.
- No validation — add bounds checks.
- Mutable version reference (
?ref=main) → pin. - Tight coupling between modules → loosen via data sources.
- Output removed in minor → use major bump.
When to escalate
- Module catalog strategy — coordinate.
- Public publishing — review.
- Cross-org module ownership — agreements.
Related prompts
-
Terraform Module Review Prompt
Get a senior-engineer review of a Terraform module — variable hygiene, state safety, security defaults, drift resistance.
-
Terraform Module Testing Prompt
Test Terraform modules — terraform test (1.6+), terratest (Go), unit vs integration, mocking providers.
-
Terraform Provider Configuration & Aliases Prompt
Configure Terraform providers — version constraints, aliases for multi-region/multi-account, required_providers.