Repository navigation
path.module is scan-root-relative inside nested modules, breaking file()/fileexists() #282
Description
Summary
Inside a non-root module, path.module is resolved relative to the scan root, while file() / fileexists() resolve their argument relative to the module's own directory. The two get composed, so the idiomatic file("${path.module}/data.yml") looks for moda/moda/data.yml and silently returns null.
When that value feeds a for_each, every resource keyed off it disappears from the graph entirely — a scan of the parent reports zero resources with no error.
Reproduction
root/
├── main.tf
└── moda/
├── main.tf
└── data.yml
root/main.tf:
module "a" {
source = "./moda"
}root/moda/main.tf:
locals {
items = yamldecode(file("${path.module}/data.yml"))
}
resource "google_storage_bucket" "x" {
for_each = local.items
name = each.value.name
location = each.value.location
}root/moda/data.yml:
bucket-1:
name: my-bucket
location: USActual
load_from_path("root") — the resource is gone, and locals.items is null:
{
"locals": [{"__tfmeta": {"path": "module.a.locals"}, "items": null}],
"module": [{"__tfmeta": {"path": "module.a"}, "source": "./moda"}]
}load_from_path("root/moda") — scanning the module directly as its own root works fine:
{
"google_storage_bucket": [
{"__tfmeta": {"path": "google_storage_bucket.x[\"bucket-1\"]"},
"name": "my-bucket", "location": "US"}
]
}Expected
Scanning root/ should expand module.a.google_storage_bucket.x["bucket-1"] with the same attributes as the direct scan. tofu init / tofu validate both succeed on this configuration, so the HCL is valid — this is purely an evaluation issue.
Root cause evidence
Probing the path builtins from inside the nested module makes the double-prefix explicit:
locals {
mod_path = path.module
exists_rel = fileexists("data.yml")
exists_mod = fileexists("${path.module}/data.yml")
raw = file("${path.module}/data.yml")
}Scanning the parent (root/):
{"mod_path": "moda", "exists_rel": true, "exists_mod": false, "raw": null}Scanning the module directly (root/moda/):
{"mod_path": ".", "exists_rel": true, "exists_mod": true, "raw": "bucket-1:\n name: my-bucket\n location: US\n"}fileexists("data.yml") is true in both cases — so the file-resolution base is already the module directory. But path.module returns "moda" (scan-root-relative) rather than "." (module-relative), so prefixing it produces moda/moda/data.yml, which does not exist. Terraform's own semantics make path.module the module's own directory, so the two should agree.
Impact
The failure is silent and severity depends on where the value lands:
- feeding
for_each/count— every resource keyed off it vanishes from the graph; a policy scan reports "0 resources" and passes clean. For a data-driven module this is usually the whole module. - feeding an ordinary attribute — the resource survives but the attribute becomes an unresolved marker, e.g.
{"__attribute__": "local.items.[bucket-1].name", "__name__": "items"}, which can produce false positives in attribute-checking policies.
Since nothing reaches the consumer in the for_each case, this cannot be worked around downstream.
Version
tfparse 0.6.20 (latest on PyPI at time of filing), reproduced both via tfparse.load_from_path() directly and through c7n-left 0.3.38.
Possibly related
- Missing submodule parsed data #71 (missing submodule parsed data, also involving
path.module) - several terraform functions with issues #193 (several terraform functions with issues, incl.
file()/fileexists()withpath.module— but in root-module context)
Activity
tvansteenburgh commentedon Aug 14, 2026
Root-caused and fixed upstream: cloud-custodian/trivy#17.
The path-based functions (
file,fileexists,fileset, …) were based onmodulePath, whilepath.moduleis itself project-root relative, so the two compose andfile("${path.module}/x")resolves<module>/<module>/x. The fix bases those functions onprojectRootPath, matching Terraform, which resolves them relative to the working directory rather than the module — which is precisely why thepath.moduleprefix is idiomatic.Behavior after the fix, for a nested module
moda:path.module"moda""moda"(unchanged)file("${path.module}/data.yml")nullfile("data.yml")That second row is a deliberate behavior change and matches Terraform, where the bare form does not read the module's own directory either. The root module is entirely unaffected, since
projectRootPath == modulePaththere.Verified locally against this repo: full suite green including
test_funcs(the existing coverage of these builtins), plus a new regression fixture that fails withKeyError: 'aws_s3_bucket'without the patch. The downstreamc7n_leftsuite is unchanged at 84 passed / 3 skipped / 1 xfailed.A follow-up PR here will add the regression fixture/test and bump the trivy
replacepin once #17 lands.fix: resolve path-based functions relative to the project root (#17)
ajkerrigan commentedon Aug 21, 2026
Okie, with cloud-custodian/trivy#17 merged we should be good to bump the pin here and get a new tfparse release out. In preparation for that...
We could just bump the tfparse pin to cloud-custodian/trivy@bdad140 and bump again + release after sorting out the upstream v0.74.0-related issues. I'm cool either way.
ajkerrigan commentedon Aug 21, 2026
Found one more test breakage, which made it feel really silly that I didn't validate tfparse end to end locally against the trivy fix. So:
@tvansteenburgh - reviewing/merging those 2 should let us get this sorted out in c7n-left, but let me know if anything smells funny or if you spot something I missed.
ajkerrigan commentedon Aug 24, 2026
This should be resolved now, and is live in c7n-left as well since cloud-custodian/cloud-custodian#11045 landed on main.
chore: Upgrade c7n-left to 0.3.38 (#84)