How to Fix Terraform 'Error: Module Not Found' for Relative Path Sources
Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 5 mins
TL;DR
- What broke: Terraform's module loader resolves
sourcepaths relative to the.tffile containing themoduleblock — not the working directory where you ranterraform init. Your path is anchored to the wrong directory. - How to fix it: Recount the
../traversals from the calling module file's location to the target module directory, not from your shell's$PWD. - Shortcut: Use our Client-Side Sandbox below to auto-refactor this — paste your broken
moduleblock and get the corrected relative path instantly.
The Incident (What Does the Error Mean?)
Raw error output from terraform init:
Error: Module not found
on main.tf line 3, in module "vpc":
3: source = "../modules/vpc"
Cannot find module "../modules/vpc"
Terraform performs module discovery at init time by walking the source path starting from the directory containing the .tf file that declares the module block. If that resolved absolute path doesn't exist on disk, the entire init fails — no state is read, no providers are downloaded for that module, and every subsequent plan or apply is dead.
This is a hard stop. There is no partial init. Your pipeline is broken.
The Attack Vector / Blast Radius
This isn't a security misconfiguration, but the blast radius in a CI/CD pipeline is total:
- Every environment sharing this root module (
dev,staging,prod) fails simultaneously if they reference the same broken source path. - Atlantis / Terraform Cloud runs queue up and fail, potentially blocking all infra PRs across teams.
- In a monorepo, a single bad path in a shared
modules/wrapper causes every downstream consumer to break on their nextinit. - If this is a nested module (a module calling another module), the path must be relative to the nested module's own directory, which engineers routinely miscalculate when restructuring directory trees.
The most common trigger: directory restructuring. Someone moves a module folder one level deeper, updates the path in their head, but anchors it from the repo root instead of the calling file.
How to Fix It (The Solution)
Verify the Actual Directory Structure First
# From repo root, confirm the module exists
find . -type d -name "vpc"
# Example output: ./infrastructure/modules/vpc
# Check where your calling .tf file lives
realpath environments/prod/main.tf
# Output: /repo/environments/prod/main.tf
Now calculate the relative path from /repo/environments/prod/ to /repo/infrastructure/modules/vpc:
../../infrastructure/modules/vpc
Basic Fix
module "vpc" {
- source = "../modules/vpc"
+ source = "../../infrastructure/modules/vpc"
cidr_block = "10.0.0.0/16"
}
After correcting the path:
terraform init
# Should resolve: Initializing modules... module.vpc
Enterprise Best Practice
Hardcoding relative paths across a large monorepo is a maintenance liability. Enforce one of these patterns:
Option 1: Terraform Registry / Git Source (eliminate relative paths entirely)
module "vpc" {
- source = "../../infrastructure/modules/vpc"
+ source = "git::https://github.com/your-org/terraform-modules.git//vpc?ref=v1.4.2"
cidr_block = "10.0.0.0/16"
}
Option 2: Monorepo with enforced path convention via terragrunt
# terragrunt.hcl
terraform {
- source = "../modules//vpc"
+ source = "${get_repo_root()}/infrastructure/modules//vpc"
}
get_repo_root() resolves from the git root — immune to directory depth changes.
💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.
Prevention in CI/CD
1. terraform init as a Lint Gate
Run terraform init -backend=false in every PR pipeline. It validates module paths without touching remote state.
# GitHub Actions example
- name: Validate Module Paths
run: |
terraform init -backend=false
terraform validate
2. Checkov — Catch Structural Issues Pre-Init
checkov -d . --check CKV_TF_1
# CKV_TF_1 enforces versioned module sources (discourages brittle relative paths)
3. tflint with Deep Module Checking
tflint --module
# Resolves and lints all relative module sources; fails on unresolvable paths
4. OPA / Conftest Policy to Ban Unversioned Relative Sources in Shared Modules
# policy/module_source.rego
package terraform
deny[msg] {
module := input.module[_][_]
source := module.source
startswith(source, "../")
not startswith(source, "../internal/") # allowlist internal-only paths
msg := sprintf("Module source '%v' uses a relative path. Use a versioned git source.", [source])
}
conftest test main.tf --policy policy/
5. Directory Structure Contract (Document It)
Maintain a MODULES.md at repo root. When anyone restructures directories, the PR checklist requires updating this doc — which forces conscious awareness of path breakage.