DevOpsTerraform Bulk Import: Bring Existing AWS Resources Under Management
Import dozens of existing AWS resources into Terraform at once using import blocks, for_each, and generate-config-out.
DevOps
Terraform module best practices for 2026. Learn module structure, versioning, composition, testing, when to create modules, and when not to over-abstract.

Modules are Terraform's unit of reuse — a directory of .tf files you can call from multiple places. Good modules save hours. Bad modules create more problems than they solve. Here's how to build modules that work.
modules/
└── vpc/
├── main.tf # Resources
├── variables.tf # Input variables
├── outputs.tf # Output values
├── versions.tf # Provider and Terraform version constraints
├── README.md # Documentation
└── examples/
└── basic/
└── main.tf # Usage exampleterraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
}
}Don't pin exact provider versions in modules — let the caller decide:
# ❌ Too restrictive — breaks when caller uses 5.50
version = "= 5.40.0"
# ✅ Minimum version with flexibility
version = ">= 5.0"Create a module when:
Don't create a module when:
# ❌ Pointless wrapper — just use the resource directly
module "bucket" {
source = "./modules/s3-bucket"
name = "my-bucket"
}
# modules/s3-bucket/main.tf
resource "aws_s3_bucket" "this" {
bucket = var.name
}
# ✅ Useful module — bundles encryption, versioning, lifecycle, and policy
module "bucket" {
source = "./modules/secure-bucket"
name = "my-bucket"
retention_days = 90
}# modules/ecs-service/variables.tf
variable "name" {
type = string
description = "Service name"
}
# Opinionated default — most services need this
variable "health_check_grace_period" {
type = number
default = 60
}
# Opinionated default — production-ready
variable "enable_container_insights" {
type = bool
default = true
}
# Flexible — caller decides
variable "desired_count" {
type = number
description = "Number of tasks"
# No default — caller must think about this
}# ❌ Module creates its own provider — breaks multi-region
# modules/vpc/main.tf
provider "aws" {
region = var.region
}
# ✅ Module inherits provider from caller
# The caller passes the provider
module "vpc_us" {
source = "./modules/vpc"
providers = { aws = aws.us_east_1 }
}
module "vpc_eu" {
source = "./modules/vpc"
providers = { aws = aws.eu_west_1 }
}# modules/vpc/outputs.tf
# ✅ Expose commonly needed attributes
output "vpc_id" {
value = aws_vpc.this.id
}
output "private_subnet_ids" {
value = aws_subnet.private[*].id
}
output "public_subnet_ids" {
value = aws_subnet.public[*].id
}
output "vpc_cidr_block" {
value = aws_vpc.this.cidr_block
}
# ✅ Expose the full resource when needed
output "vpc" {
value = aws_vpc.this
description = "Full VPC resource (for advanced use)"
}# Resource names inside modules: use "this" for the primary resource
resource "aws_vpc" "this" { ... }
resource "aws_s3_bucket" "this" { ... }
# If multiple resources of the same type: use descriptive names
resource "aws_subnet" "public" { ... }
resource "aws_subnet" "private" { ... }Compose small modules into larger ones:
# modules/platform/main.tf — composes smaller modules
module "vpc" {
source = "../vpc"
vpc_cidr = var.vpc_cidr
}
module "database" {
source = "../rds"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
}
module "cluster" {
source = "../ecs"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
}Root config
└── module "platform"
├── module "vpc"
├── module "database"
└── module "cluster"Keep nesting shallow — max 2-3 levels. Deep nesting makes debugging hard.
module "vpc" {
source = "git::https://github.com/company/terraform-modules.git//modules/vpc?ref=v1.2.0"
}module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0" # Compatible 5.x releases
}version = "5.0.0" # Exact
version = "~> 5.0" # >= 5.0.0, < 6.0.0
version = ">= 5.0" # 5.0 or higher
version = ">= 5.0, < 6.0" # Explicit rangeUse ~> for modules — allows patch/minor updates, blocks breaking changes.
# tests/vpc.tftest.hcl
run "vpc_basic" {
command = plan
variables {
vpc_cidr = "10.0.0.0/16"
environment = "test"
}
assert {
condition = aws_vpc.this.cidr_block == "10.0.0.0/16"
error_message = "VPC CIDR mismatch"
}
assert {
condition = length(aws_subnet.private) == 3
error_message = "Expected 3 private subnets"
}
}Every module should have an examples/ directory:
# modules/vpc/examples/basic/main.tf
module "vpc" {
source = "../../"
vpc_cidr = "10.0.0.0/16"
environment = "example"
azs = ["us-east-1a", "us-east-1b"]
}
output "vpc_id" {
value = module.vpc.vpc_id
}# ❌ One module that does everything
module "infrastructure" {
source = "./modules/everything"
# 50 variables for VPC + ECS + RDS + CloudFront + Route53 + ...
}
# ✅ Compose focused modules
module "networking" { source = "./modules/vpc" }
module "database" { source = "./modules/rds" }
module "compute" { source = "./modules/ecs" }# ❌ Adds nothing — just use the resource
module "sg" {
source = "./modules/security-group"
name = "web"
vpc_id = module.vpc.vpc_id
ingress_port = 443
}# ❌ Caller must know internal resource structure
output "web_instance_private_dns" {
value = aws_instance.web_servers["primary"].private_dns
}
# ✅ Clean interface
output "web_endpoint" {
value = aws_lb.main.dns_name
}Good modules are focused (one concern), opinionated (sensible defaults), flexible (overridable), and tested (examples + tests). Don't wrap single resources. Don't create god modules. Compose small modules into larger ones with max 2-3 levels of nesting. Version with semantic tags and use ~> constraints. The best test for a module: can a new team member use it from the README alone?
DevOpsImport dozens of existing AWS resources into Terraform at once using import blocks, for_each, and generate-config-out.
DevOpsLearn Terraform data sources to read existing AWS resources, look up AMIs, query remote state, and reference external information in your configurations.
DevOpsLearn when to use Terraform depends_on for explicit resource dependencies. Understand implicit vs explicit dependencies, common use cases
DevOpsLearn Terraform locals to reduce duplication, compute values, and organize complex configurations. Practical examples with naming conventions, tag maps