Module dependencies in Terraform create challenges when outputs need to pass between modules or when circular references occur. This guide covers diagnosing and fixing module dependency problems.
Understanding Module Dependencies
Terraform automatically infers dependencies from resource references, but sometimes explicit dependencies are needed. Common errors:
Error: Module "vpc" has no output named "subnet_ids"
Error: Cycle detected: module.vpc -> module.security -> module.vpc
Error: Resource 'aws_subnet.public' not found for variable referenceIssue 1: Missing Module Outputs
Referencing outputs that don't exist in the source module.
Error Example:
``
Error: Unsupported attribute on main.tf line 25:
25: subnet_ids = module.vpc.subnet_ids
This object does not have an attribute named "subnet_ids"
Root Cause:
The module doesn't export the expected output:
``hcl
# modules/vpc/main.tf
resource "aws_subnet" "public" {
count = 3
vpc_id = aws_vpc.main.id
cidr_block = var.public_subnet_cidrs[count.index]
}
# Missing output!
Solution:
Add the missing output to the module: ```hcl # modules/vpc/outputs.tf output "subnet_ids" { description = "List of public subnet IDs" value = aws_subnet.public[*].id }
output "vpc_id" { description = "The VPC ID" value = aws_vpc.main.id }
output "subnet_cidr_blocks" { description = "CIDR blocks of the subnets" value = aws_subnet.public[*].cidr_block } ```
Now reference it correctly: ```hcl module "ec2" { source = "./modules/ec2"
subnet_ids = module.vpc.subnet_ids # Now works } ```
Issue 2: Circular Dependencies
Modules that depend on each other create cycles.
Error Example:
``
Error: Cycle: module.vpc -> module.security -> module.vpc
Root Cause: ```hcl # Circular dependency module "vpc" { source = "./modules/vpc" sg_ids = module.security.vpc_sg_ids # Needs security group }
module "security" { source = "./modules/security" vpc_id = module.vpc.vpc_id # Needs VPC } ```
Solution:
Restructure to break the cycle: ```hcl # Option 1: Create shared resources at root level resource "aws_vpc" "main" { cidr_block = var.vpc_cidr }
resource "aws_security_group" "vpc_sg" { vpc_id = aws_vpc.main.id }
module "subnets" { source = "./modules/subnets" vpc_id = aws_vpc.main.id }
module "security" { source = "./modules/security" vpc_id = aws_vpc.main.id }
# Option 2: Pass security group ID to VPC module via variable module "vpc" { source = "./modules/vpc" }
module "security" { source = "./modules/security" vpc_id = module.vpc.vpc_id }
# Security groups created after VPC exists # Then update VPC resources that need SG IDs ```
Issue 3: Implicit vs Explicit Dependencies
Resources created in wrong order due to missing dependency declarations.
Error Scenario:
``
# Resource A depends on Resource B but Terraform creates A first
# This happens when there's no direct reference
Solution:
Use depends_on for explicit dependencies:
```hcl
module "network" {
source = "./modules/network"
}
module "kubernetes" { source = "./modules/kubernetes"
network_id = module.network.vpc_id # Implicit dependency
# Explicit dependency for resources not directly referenced depends_on = [ module.network.nat_gateway, module.network.vpc_endpoint ] } ```
For resources: ```hcl resource "aws_s3_bucket" "logs" { bucket = "application-logs" }
resource "aws_iam_role_policy" "s3_access" { role = aws_iam_role.app.id policy = data.aws_iam_policy_document.s3.json
depends_on = [aws_s3_bucket.logs] } ```
Issue 4: Module Source Changes
Changing module source triggers full replacement.
Error Example:
``
Error: Module source has changed
Old: ./modules/vpc
New: git::https://github.com/org/modules//vpc
Solution:
Properly manage module source changes: ```bash # When changing module sources terraform init -upgrade
# Force reinitialization rm -rf .terraform/modules terraform init ```
Use versioning for remote modules: ```hcl module "vpc" { source = "terraform-aws-modules/vpc/aws" version = "~> 3.0" # Pin version
name = "production" cidr = var.vpc_cidr } ```
Issue 5: Module Output Type Mismatches
Output type doesn't match expected type in consuming module.
Error Example:
``
Error: Invalid value for module argument on main.tf line 10:
10: subnet_ids = module.vpc.subnet_ids
The given value is not suitable for child module variable
"subnet_ids" defined at modules/ec2/variables.tf:5:
string required, but got list of strings.
Solution:
Ensure output and variable types match: ```hcl # modules/vpc/outputs.tf output "subnet_ids" { description = "List of subnet IDs" value = aws_subnet.public[*].id # Returns list }
# modules/ec2/variables.tf variable "subnet_ids" { description = "List of subnet IDs" type = list(string) # Must match output type } ```
For complex types: ```hcl # Output complex object output "network_config" { value = { vpc_id = aws_vpc.main.id subnet_ids = aws_subnet.public[*].id cidr_blocks = aws_subnet.public[*].cidr_block } }
# Variable with matching type variable "network_config" { type = object({ vpc_id = string subnet_ids = list(string) cidr_blocks = list(string) }) } ```
Issue 6: For Each with Module Outputs
Using module outputs incorrectly with for_each.
Error Example:
``
Error: Invalid for_each argument on main.tf line 15:
15: for_each = module.vpc.subnets
The given value is not suitable for "for_each".
Solution:
Ensure module output is a map for for_each: ```hcl # Module output must be a map # modules/vpc/outputs.tf output "subnets" { value = { for i, subnet in aws_subnet.public : "subnet-${i}" => { id = subnet.id cidr_block = subnet.cidr_block az = subnet.availability_zone } } }
# Usage resource "aws_instance" "web" { for_each = module.vpc.subnets
subnet_id = each.value.id # ... }
# Alternative: Convert list to map with tomap() output "subnet_map" { value = tomap({ for subnet in aws_subnet.public : subnet.availability_zone => subnet.id }) } ```
Issue 7: Module Timeouts and Slow Dependencies
Modules with long-running resources cause timeout issues for dependents.
Error Example:
``
Error: timeout waiting for state to become 'available'
Solution:
Add explicit timeouts and dependencies: ```hcl resource "aws_db_instance" "main" { # ... configuration ...
timeouts { create = "2h" update = "2h" delete = "2h" } }
resource "aws_instance" "app" { # Wait for database depends_on = [aws_db_instance.main]
# ... configuration ... }
# Or use null_resource for explicit wait resource "null_resource" "wait_for_db" { depends_on = [aws_db_instance.main]
provisioner "local-exec" { command = "sleep 60" # Wait for DB to be fully ready } } ```
Verification Steps
Check module dependencies: ```bash # Visualize dependency graph terraform graph | dot -Tpng > graph.png
# View module outputs terraform output -module=vpc
# List all module resources terraform state list | grep module ```
Test module in isolation:
``bash
# Test module independently
cd modules/vpc
terraform init
terraform plan
Best Practices
- 1.Define all outputs in a separate
outputs.tffile within each module - 2.Use explicit
depends_ononly when necessary (implicit via references is preferred) - 3.Structure modules to have clear inputs and outputs
- 4.Avoid circular dependencies by restructuring resources
- 5.Use
movedblocks when refactoring module relationships - 6.Document module dependencies in README files