症状:你的 Lambda 部署炸了
事情是这样的。上周五下午,我在部署一个 Lambda 函数,代码结构大概是这样的:
my-project/
├── terraform/
│ ├── main.tf
│ ├── modules/
│ │ └── lambda/
│ │ └── main.tf
│ └── lambda_code/
│ └── index.js
然后在 modules/lambda/main.tf 里,我用了个 archive_file 数据源去 zip 代码:
data "archive_file" "lambda_zip" {
type = "zip"
source_dir = "${path.module}/../../lambda_code"
output_path = "${path.module}/lambda.zip"
}
结果呢?terraform plan 跑起来直接报错:
Error: Error reading source directory: open /some/absolute/path/terraform/modules/lambda/../../lambda_code: no such file or directory
我盯着路径看了五分钟,确认没写错。然后试了 path.root,还是不行。群里一问,发现踩这个坑的人比我想象的多得多。
根因分析:为什么 path.module 和 path.root 都不靠谱?
这里有个 Terraform 的经典认知误区。
path.module 指向的是当前模块文件所在的目录。在子模块 modules/lambda 里用 path.module,它解析的就是 /project/terraform/modules/lambda。
所以 ../../lambda_code 会解析成 /project/terraform/lambda_code —— 这路径是对的。但问题在于:Terraform 在 apply 之前,会先检查路径是否存在。如果你的 terraform 目录不是从项目根目录 my-project 执行的,或者你的 CI/CD pipeline 把 .tf 文件复制到了临时目录,这个相对路径就彻底断掉了。
path.root 指向的是根模块的目录,也就是你执行 terraform apply 的那个目录。这听起来更靠谱?但它的坑在于:如果你的根模块不在项目根目录(比如很多人把 Terraform 配置放在 infra/ 子目录里),path.root 解析出来的路径依然不对。
最恶心的场景是 Terraform Cloud 远程执行 —— 你的代码被上传到 Terraform 的沙箱环境后,所有本地相对路径全部失效。这问题在 Reddit 和 HN 上被骂了不知道多少次。
解决方案:三种硬核姿势
方案一:用 path.cwd + 绝对化路径(最推荐)
path.cwd 指向你执行 terraform 命令时所在的当前工作目录。这玩意儿在本地和 CI 里表现一致,但有个前提:你必须从项目根目录执行 terraform。
# 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() 函数会把相对路径转成绝对路径。这样无论 Terraform 怎么复制模块文件,只要执行目录正确,路径就能解析。
缺点:如果你的项目结构变了,或者有人从子目录执行 terraform,还是炸。
方案二:用 path.root + 项目根目录约束(结构清晰)
把 Terraform 根模块放在项目根目录,然后在所有子模块里统一用 path.root 做锚点:
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"
}
这方案最干净,但要求你强制团队遵循目录约定。大项目里容易出幺蛾子。
方案三:传变量,彻底解耦(最工程化)
把路径作为变量传入,让调用者决定:
# 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")
}
这是最稳妥的做法。路径的解析责任完全交给了调用方,子模块不需要关心外面长什么样。
三种方案对比
| 方案 | 适用场景 | 优点 | 坑点 |
|---|---|---|---|
path.cwd + abspath | 小型项目、快速原型 | 改动最小,一行搞定 | 依赖执行目录,CI 中容易翻车 |
path.root + 目录约束 | 中大型项目,团队规范好 | 路径清晰,可读性强 | 强制目录结构,重构成本高 |
| 变量传参 | 公共模块、多环境部署 | 完全解耦,最稳健 | 调用方需要多写几行代码 |
社区的真实反馈
Reddit 上有个帖子问 “What is the idiomatic way of importing data files outside module?",下面高赞回答直接开喷:“Terraform’s path handling is garbage for cross-module references. Just pass it as a variable and move on.”
还有一个兄弟在 HN 上吐槽,说他们的 Terraform Cloud pipeline 因为路径问题炸了三天,最后发现是 Terraform 把代码上传后,所有相对路径都变成了相对于沙箱根目录。解决方案?“We ended up writing a Makefile wrapper that copies the files into the module directory before running terraform.” —— 这操作属实硬核,但也说明这问题有多恶心。
我个人觉得,Terraform 在跨模块文件引用这块的设计确实拉胯。path.module 和 path.root 的语义在子模块里经常让人迷惑,文档又写得很模糊。如果你在做一个会被别人复用的公共模块,千万别依赖任何外部路径,老老实实传变量。
FAQ
Q: 为什么 path.module 在子模块里指向的是模块目录而不是根目录?
A: 因为 path.module 的设计意图就是让模块内部引用自己的文件,比如模板文件、脚本等。它从设计上就不应该知道外部目录的存在。这是有意为之,不是 bug。
Q: 在 Terraform Cloud 中如何解决外部文件引用?
A: Terraform Cloud 远程执行时,你的代码被上传到临时沙箱。任何本地路径都会炸。解决方案:1) 使用 filebase64sha256() 等函数直接读取文件内容作为变量传递;2) 在 CI pipeline 中把文件复制到模块目录内再上传;3) 使用外部数据源(如 AWS S3)存储文件。
Q: path.cwd 和 path.root 有什么区别?
A: path.cwd 是你执行 terraform 命令时的当前工作目录。path.root 是根模块的目录(即包含 main.tf 的那个目录)。如果你在根模块目录里执行 terraform,两者相同。如果从其他目录执行(比如 terraform -chdir=infra/),两者就不同了。
Q: 有没有办法让子模块自动检测项目根目录?
A: 没有内置机制。Terraform 的模块系统故意不做这种 “向上查找” 的逻辑,因为这会破坏模块的可移植性和确定性。第三方工具如 Terragrunt 提供了 find_in_parent_folders() 函数,但那是 Terragrunt 的功能,不是 Terraform 的。
社区灵感与参考 (References & Community Insights)
本文探讨的架构演进与技术实现方案,深度提炼自 Hacker News、Reddit 等极客社区的真实工程师讨论、线上事故复盘(Post-mortems)以及一线技术博客的实战经验分享。