Ops Notes

Terraform Module Can't Access Files Outside Root? Fix Cross-Directory Path Hell Once and For All

Cloud & DevOps Visualization

The Symptom: Your Lambda Deployment Just Exploded

Last Friday, 3 PM. I’m deploying a Lambda function. Directory structure looks like this:

my-project/
├── terraform/
│   ├── main.tf
│   ├── modules/
│   │   └── lambda/
│   │       └── main.tf
│   └── lambda_code/
│       └── index.js

In modules/lambda/main.tf, I’m using archive_file to zip the code:

data "archive_file" "lambda_zip" {
  type        = "zip"
  source_dir  = "${path.module}/../../lambda_code"
  output_path = "${path.module}/lambda.zip"
}

terraform plan hits me with:

Error: Error reading source directory: open /some/absolute/path/terraform/modules/lambda/../../lambda_code: no such file or directory

I stare at the path for five minutes. It’s correct. Then I try path.root. Same crap. I ping the team Slack and find out half the team has hit this exact wall.

Root Cause: Why Both path.module and path.root Are Garbage Here

Here’s the classic Terraform cognitive trap.

path.module resolves to the directory containing the current module file. Inside modules/lambda, it becomes /project/terraform/modules/lambda.

So ../../lambda_code resolves to /project/terraform/lambda_code — which is technically correct. But here’s the kicker: Terraform checks path existence before apply. If your working directory isn’t my-project, or your CI pipeline copies .tf files to a temp dir, that relative path breaks completely.

path.root points to the root module directory — where you ran terraform apply. Sounds better? The trap: if your root module isn’t at the project root (like many teams with an infra/ subdirectory), path.root still resolves wrong.

The absolute worst case is Terraform Cloud remote execution. Your code gets uploaded to a sandbox environment, and every local relative path dies. This has been a recurring complaint on Reddit and HN for years.

The Fix: Three Battle-Tested Approaches

Approach 1: path.cwd + abspath() (Quickest Fix)

path.cwd points to your current working directory when you ran terraform. It’s consistent between local and CI — as long as you always run terraform from the project root.

# modules/lambda/main.tf
data "archive_file" "lambda_zip" {
  type        = "zip"
  source_dir  = "${abspath("${path.cwd}/../lambda_code")}"
  output_path = "${path.module}/lambda.zip"
}

abspath() converts the relative path to absolute. No matter how Terraform copies module files, as long as the execution directory is right, it works.

Downside: if someone runs terraform from a subdirectory, it breaks again.

Approach 2: path.root + Project Root Constraint (Cleanest Structure)

Put your root module at the project root, then use path.root as the anchor everywhere:

my-project/
├── main.tf
├── modules/
│   └── lambda/
│       └── main.tf
└── lambda_code/
    └── index.js
# modules/lambda/main.tf
data "archive_file" "lambda_zip" {
  source_dir  = "${path.root}/lambda_code"
  output_path = "${path.module}/lambda.zip"
}

This is the cleanest option, but forces a strict directory convention across your team. Large projects tend to fight this.

Approach 3: Pass as Variable (Most Engineering)

Decouple the path entirely — let the caller decide:

# modules/lambda/variables.tf
variable "source_code_path" {
  type        = string
  description = "Absolute or relative path to lambda source code"
}

# modules/lambda/main.tf
data "archive_file" "lambda_zip" {
  source_dir  = var.source_code_path
  output_path = "${path.module}/lambda.zip"
}
# main.tf
module "my_lambda" {
  source           = "./modules/lambda"
  source_code_path = abspath("${path.module}/lambda_code")
}

This is the most robust approach. The submodule doesn’t need to know anything about the outside world. Path resolution is the caller’s problem.

Comparison Table

ApproachUse CaseProsCons
path.cwd + abspath()Small projects, quick prototypesMinimal changes, one-linerDepends on execution directory, CI issues
path.root + dir constraintMedium-large teams with conventionsClear paths, readableForces directory structure, costly refactors
Variable passthroughPublic modules, multi-environmentFully decoupled, most robustCaller writes a few extra lines

Community Reality Check

Reddit thread asking “What is the idiomatic way of importing data files outside module?” — top comment literally says: “Terraform’s path handling is garbage for cross-module references. Just pass it as a variable and move on.”

Another HN commenter described their Terraform Cloud pipeline breaking for three days because of path issues. Their solution? “We ended up writing a Makefile wrapper that copies the files into the module directory before running terraform.” Brutal, but it worked.

Here’s my take: Terraform’s cross-module file reference design is genuinely bad. path.module and path.root have confusing semantics inside child modules, and the docs are vague on this part. If you’re writing a reusable public module, never depend on external paths. Always pass variables.

FAQ

Q: Why does path.module point to the module directory instead of the root directory in a child module? A: Because path.module is designed for a module to reference its own internal files — templates, scripts, etc. It’s intentionally unaware of external directories. This is by design, not a bug.

Q: How do you handle external file references in Terraform Cloud? A: Terraform Cloud remote execution uploads your code to a temporary sandbox. Any local path will break. Solutions: 1) Use functions like filebase64sha256() to read file content as a variable; 2) Copy files into the module directory in your CI pipeline before upload; 3) Use external data sources like S3 to store files.

Q: What’s the difference between path.cwd and path.root? A: path.cwd is your current working directory when you ran terraform. path.root is the root module directory (where main.tf lives). If you run terraform from the root module directory, they’re the same. If you use terraform -chdir=infra/, they differ.

Q: Is there a way for a child module to auto-detect the project root? A: No built-in mechanism. Terraform’s module system deliberately avoids “upward search” logic because it breaks module portability and determinism. Third-party tools like Terragrunt offer find_in_parent_folders(), but that’s Terragrunt, not Terraform.

References & Community Insights

The following authoritative resources were referenced for architectural best practices and specifications:

Elvin Hui

About the Author: Elvin Hui

Elvin is a Senior Infrastructure Engineer with 10+ years of experience spanning data centers, cloud-native architecture, and network security. Certified in CCNA and AWS Solutions Architecture, I focus on turning real-world production "war stories" into actionable, hardcore technical guides.