从前有个食堂后厨,大厨老王每天忙得脚不沾地——因为每一道菜都有人点,但每次做菜他都要从零开始:洗菜、切菜、试味道、调火候。直到有一天,他把最受欢迎的“红烧肉”写成了标准菜谱:用料清单、步骤、火候、时间全都固定下来。从此不管谁掌勺,只要照着菜谱做,端出来的味道都一模一样;想微调口味,改一改“少糖”“多辣”这两个参数就行。这就是“模块化”的魔力。 Terraform Module 干的就是这件事——把云上的基础设施(网络、服务器、数据库)写成一份份“标准菜谱”,让团队不用每次从零搭建,而是像点菜一样传几个参数就能吃到同一道“菜”。本文将用这套“后厨菜谱”的比喻,带你彻底搞懂 Module 怎么写、变量和输出怎么用、版本怎么管、目录怎么组织。

岗位关联

学习 Terraform Module,等于掌握了基础设施复用的“标准菜谱”。对从事 IaC 和云基础设施管理的工程师来说,这是把重复劳动变成标准化资产的核心技能,能显著提升团队协作效率和云资源管理的规范性。

目录
  1. 可视化演示
  2. 模块化的价值
  3. 模块结构规范
  4. 变量与输出
  5. 依赖与来源
  6. 版本管理策略
  7. 最佳实践清单
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示Terraform Module模块化设计:标准目录结构、变量输入输出规范、版本管理与依赖控制、模块仓库共享及最佳实践反模式。切换模式查看不同环节。

本节配有交互动效(进入视口后自动加载,约 3KB)。

Module架构与目录结构 标准目录含main.tf/variables.tf/outputs.tf,职责清晰 根模块组合子模块,通过变量输入输出传递配置 root → module(var) → output 模块化架构实现基础设施代码复用与组合

模块化的价值

老王的菜谱让后厨从“手忙脚乱”变成了“井井有条”。以前每个厨师都有自己的“秘方”,做出来的菜味道飘忽不定;现在所有热销菜都有标准菜谱,任何人上手都能做出一致的水准。Terraform Module 的价值,正是把这种“标准化力量”引入云基础设施管理。

没有 Module 之前,团队创建 VPC、安全组、ECS 集群时,最常用的操作是“复制粘贴上一个环境的配置”,然后手动改几个名字和网段。这就像每家店都自己发明红烧肉的做法——味道全凭手感,一旦某个厨师离职,新来的厨师对着旧配置根本不敢动。

Terraform Module 把一组反复使用的基础设施定义打包成一个可复用的独立单元:外部只关心“传入哪些参数、得到哪些输出”,内部资源怎么组合一概隐藏。这样写一次、验一次,之后所有环境都是同一条流水线生产出来的“标准件”。

回到后厨:模块化让老王拥有了“随时休假”的底气——菜谱在,味道就在。同样,当你的核心基础设施都模块化之后,无论团队怎么变动,云上架构都能保持稳定、可预期。

重复代码消除率=复制粘贴的配置数−模块化后的配置数复制粘贴的配置数×100%\text{重复代码消除率} = \frac{\text{复制粘贴的配置数} - \text{模块化后的配置数}}{\text{复制粘贴的配置数}} \times 100\%
这个公式不是在 Terraform 里敲的命令,而是帮你衡量模块化收益的“体检指标”:分母是原来的重复量,分子是减少的量。比值越高,说明模块化越成功。
  1. 复制粘贴的配置数指改造前多个环境各自的 .tf 文件数量总和。比如生产、测试、预发三套环境各有 6 个文件,总数就是 18。
    复制粘贴的配置数=18\text{复制粘贴的配置数} = 18
  2. 模块化后的配置数指用模块之后,实际需要维护的配置数量。三套环境共用同一个模块,只需在调用处传参,配置数降为 3。
    模块化后的配置数=3\text{模块化后的配置数} = 3
  3. 最终计算把 18 和 3 代入公式,消除率高达 83%,意味着团队只维护原来 17% 的代码量。
    18−318×100%=83.3%\frac{18 - 3}{18} \times 100\% = 83.3\%

模块结构规范

菜谱要写得规范才能让任何人看懂。Terraform Module 也有自己约定俗成的“书写格式”:一个模块就是一个普通目录,目录里的文件按职责分工存放。如同菜谱要分“食材清单、做法步骤、装盘要点”三部分。

一个标准 Module 目录里通常有四个关键文件:main.tf 是核心资源的定义(相当于“做法步骤”);variables.tf 声明外部可传入的参数(相当于“食材清单的变量项”);outputs.tf 定义要暴露出去的结果(相当于“装盘要点”);versions.tf 则声明 provider 和 Terraform 的版本要求。这种结构不是强制语法,而是社区沉淀出的最佳习惯——因为“一眼能找到菜谱的每个部分”,是协作效率的基础。

调用模块时,根模块(你工作目录里的 .tf 文件)通过 module 块加载子模块,并传入参数。模块可以引用其他模块,一层套一层,就像菜谱里写着“酱汁的做法参见第 42 页”。

现在再看你自己的基础设施:如果每个项目目录下都堆着几十个 .tf 文件,说明你还在“手写每一道菜”。把它们按职责拆进独立目录、明确入口和出口,你便拥有了第一本自己的菜谱。

Module:=⟨source,inputs,outputs⟩\text{Module} := \langle \text{source}, \text{inputs}, \text{outputs} \rangle
这是一个“模块配方”的抽象表达:三个要素分别是“菜谱从哪里来”“需要哪些原料”“最后端出什么”。所有 Terraform 模块,本质上都是这三个要素的组合。
  1. Module表示一个完整的、可复用的模块实例,对应菜谱成品本身。
    Module\text{Module}
  2. source模块代码的来源地址,可以是本地路径 ./modules/vpc,也可以是 Git 仓库或公共注册中心。这是菜谱的“出处”。
    source=本地路径或URL\text{source} = \text{本地路径或URL}
  3. inputs模块入口参数。调用方通过变量传入配置值,就像点菜时告诉厨师“少辣、多葱”。
    inputs={vpc_name,cidr_block}\text{inputs} = \{ \text{vpc\_name}, \text{cidr\_block} \}
  4. outputs模块对外暴露的返回值,供调用方或其他模块引用。相当于上菜时标注“本菜含花生,过敏者注意”。
    outputs={vpc_id,subnet_ids}\text{outputs} = \{ \text{vpc\_id}, \text{subnet\_ids} \}

变量与输出

菜谱如果写死“五花肉 500 克”,就永远只能做一道红烧肉。老王把菜谱改成“五花肉(用量按人数调整)”,于是 4 人餐和 20 人宴席都能用同一本菜谱。Terraform Module 的变量,就是这个“按人数调整”的旋钮。

在 variables.tf 中,每个 variable 块声明一个入口参数:可以指定类型(string、number、bool、list、map),写清楚 description(这个参数是干嘛的),还可以给默认值。调用方在 module 块里通过 参数名 = 值 的方式传入。如果某个变量没有默认值、调用方又没传,Terraform 会在 plan 阶段直接报错——就像菜谱写着“必须放盐”但你没放,厨师会停下来问你。

输出是模块的“透明窗口”。模块内部创建了许多资源,但外部怎么拿到它们的 ID?用 output 块。比如 VPC 模块创建完 VPC 后,通过 output "vpc_id" { value = aws_vpc.main.id } 把 VPC 的 ID 暴露出来,调用方就能用 module.vpc.vpc_id 引用它。没有输出,模块就像一道锁在厨房里的菜,谁也吃不到。

把模块接口设计好,等于给团队发了一本“点菜单”:使用者只关心有哪些参数可调、哪些结果可拿,完全不用关心后厨怎么炒菜。这就是模块化的优雅之处——标准化与灵活性,通过变量和输出达成了和解。

vin∈Variable(type,default)⇒oout∈Output(value)v_{\text{in}} \in \text{Variable}(type, default) \quad \Rightarrow \quad o_{\text{out}} \in \text{Output}(value)
这个式子表达的是模块接口的“输入-输出”法则:左边是变量入口,要求类型合法、可设默认值;右边是输出出口,把内部资源的关键值暴露给外部。
  1. v_in某个输入变量,比如 vpc_name。它必须先声明类型,才能保证调用方传值不跑偏。
    vin=vpc_name,type=stringv_{\text{in}} = \text{vpc\_name}, \text{type} = \text{string}
  2. Variable(type, default)变量声明由类型和默认值共同决定。默认值的存在,让“不传参也能工作”成为可能——就像菜谱默认不放香菜。
    Variable(string,default="my-vpc")\text{Variable}(\text{string}, \text{default} = \text{"my-vpc"})
  3. o_out某个输出值,比如 VPC ID。它是模块与外界的“握手信号”。
    oout=vpc_ido_{\text{out}} = \text{vpc\_id}
  4. Output(value)输出块内部用 value 字段指定要暴露的资源属性。整个过程形成“输入变量 → 创建资源 → 输出结果”的流水线。
    Output(value=aws_vpc.main.id)\text{Output}(\text{value} = \text{aws\_vpc.main.id})

依赖与来源

菜谱可以来自家里祖传的手抄本(本地路径),也可以从书店买的名厨出版书(公共 Registry),还可以从总店内部系统调取(Git 仓库)。Terraform Module 的 source 参数,决定菜谱从哪里来。

最常用的是本地路径:source = "./modules/vpc",适合单体仓库内复用;其次是 Git 仓库:source = "git::https://github.com/xxx/terraform-aws-vpc.git?ref=v1.2.0",可以锁定分支或 tag;还可以从 Terraform Registry(公共源)直接引用 terraform-aws-modules/vpc/aws;以及从 S3/GCS 等对象存储加载打包好的模块。无论哪种来源,terraform init 阶段都会把模块代码拉到本地 .terraform 目录。

模块之间也会互相引用。假设 A 模块的 outputs 被 B 模块的 inputs 引用,就形成一条依赖边 A → B。Terraform 把整个配置解析成一张有向无环图(DAG),先创建被依赖的 A,再创建 B。没有依赖关系的模块可以并行执行——就像后厨里蒸鱼和炒菜可以同时开火。

理解这张图很重要:当你的模块依赖乱成一团时,Terraform 会像后厨里所有人都在抢同一个灶台一样低效。反过来,依赖清晰、可并行,整个 apply 过程就像一条设计精良的流水线。

G(V,E)  where  eij=(A→B)∈E⇒create(A)≺create(B)G(V, E) \; \text{where} \; e_{ij} = (A \to B) \in E \Rightarrow \text{create}(A) \prec \text{create}(B)
这行式子描述的是模块依赖图:V 是所有模块或资源的集合,E 是依赖关系集合。箭头 A→B 表示“B 依赖 A”,执行顺序上必须先建 A 再建 B。
  1. G(V, E)一张依赖图,V 是节点(每个模块或资源),E 是依赖边。
    G(V,E)G(V, E)
  2. e_ij = (A→B)一条从 A 指向 B 的边。表示 B 的某参数引用了 A 的输出。
    eij=(A→B)e_{ij} = (A \to B)
  3. create(A) ≺ create(B)符号 ≺ 表示“先于”。A 必须先被创建,B 才能在参数中拿到 A 的值。
    create(A)≺create(B)\text{create}(A) \prec \text{create}(B)

版本管理策略

老王发现,菜谱改着改着就乱了:有人改了糖量,有人换了酱油品牌,后厨做出来的“红烧肉”又出现了三种味道。于是他规定:每次改动都要升版本号,1.x 是咸甜口,2.x 是辣口。从此没人敢在不升级号的情况下乱改配方。

Terraform Module 用语义化版本(SemVer)管理这个规则:主版本号(MAJOR)不兼容变更时递增;次版本号(MINOR)向后兼容的功能新增时递增;修订号(PATCH)向后兼容的缺陷修复时递增。调用方用版本约束运算符来锁定范围,最常用的是 ~>(允许指定版本范围内的更新)。

version=MAJOR.MINOR.PATCH,约束∼>MAJOR.MINOR\text{version} = \text{MAJOR.MINOR.PATCH}, \quad \text{约束} \sim> \text{MAJOR.MINOR}

1. MAJOR.MINOR.PATCH:版本号三段结构。1.2.3 中 1 是主版本、2 是次版本、3 是修订版。

version=1.2.3\text{version} = 1.2.3

2. ~> 运算符:允许同主版本内更新。~> 4.0 允许 4.0、4.1、4.9,但不会升到 5.0。

约束=∼>4.0\text{约束} = \sim> 4.0

3. 约束含义:如同你告诉供应商“给我这一代改良版的菜谱,但别擅自换配方体系”。

4.0≤version<5.04.0 \le \text{version} < 5.0

版本管理最重要的原则是:已发布的模块,禁止破坏性变更。新加一个变量可以,改变量类型不行;新增输出可以,删输出不行。正如菜谱一旦印刷成书,你可以加“附赠小贴士”,但不能把“白糖”改成“盐”还不通知读者。规范的发布流程应该包括:更新 CHANGELOG、打 Git tag、在 Registry 发布新版本。

版本锁得住,团队才不会出现“我这里跑得好好的,你那里就报错”的经典惨案。下次 terraform init 时看到版本约束报错,多为那本“菜谱”版本太新,超出了你的预期范围。

最佳实践清单

老王最终把后厨整理成了一家“标准化餐厅”,靠的是一套铁律。Terraform Module 也有自己的一套最佳实践,遵守它们,你的基础设施仓库会像米其林后厨一样整洁高效。

第一,单一职责,小模块优先。一个模块只做一件事:VPC 模块管网络,EKS 模块管集群,安全组模块管访问规则。不要写一个 500 行的“万能模块”,那不叫模块,叫意大利面。第二,用 for_each 和 count 替代复制。需要三个子网,就循环创建,而不是复制三遍代码——这就像菜谱写“每桌一份”而不是“第一桌一份,第二桌一份”。第三,用数据源或 remote state 替代硬编码。不要写死 vpc-12345,用 data "aws_vpc" 去查询,或者从模块输出中引用。

第四,规范目录与 CI。一个组织级的模块仓库通常用 modules/ 存模块,examples/ 存最小可用示例。CI 流水线跑 terraform fmt、terraform validate,甚至可以跑 terratest 做集成测试。这相当于每本菜谱出版前,都要经过试菜会和品控检查。模块内部通过 terraform plan 的输出确保“这次改动会不会影响已有环境”。

后厨的故事到这里就讲完了。老王的菜谱体系最终让餐厅从“靠人”变成“靠系统”:新人三天上手,菜品常年稳定,扩张不慌不忙。你的 Terraform Module 仓库,也应当成为这样的系统——它是团队基础设施的“菜谱库”,让云上世界井然有序。

quality=通过CI检查的次数代码变更总次数→100%\text{quality} = \frac{\text{通过CI检查的次数}}{\text{代码变更总次数}} \to 100\%
这不是一个真实指令,而是理想目标:每次改动都经过自动化检查,质量趋近于“百分之百被验证过”。CI 就是后厨里的“试菜员”。
  1. quality模块仓库的质量指标,用“通过检查的比例”来度量。
    quality\text{quality}
  2. 通过 CI 检查的次数每次提交代码,CI 自动执行格式和语法校验,通过的次数求总和。
    通过CI检查的次数\text{通过CI检查的次数}
  3. 代码变更总次数仓库里所有向 main 分支发起的提交数量。
    代码变更总次数\text{代码变更总次数}

AI 应用场景

场景一:AI 训练集群的标准化交付

阶段:基础设施准备(GPU 资源编排)

解决的问题:训练团队每次启动新的 GPU 集群,都需要手动配置 VPC、安全组、节点组和存储,耗时数小时且容易因配置漂移导致训练任务失败。平台工程团队将整套 GPU 集群封装成 eks-gpu-cluster 模块,通过 变量 控制实例类型、节点数量和磁盘大小,训练团队只需提交一份极简配置即可拉起集群。

具体案例:某算法团队需要用 8 卡 A100 集群跑大模型微调。他们只需在 .tf 文件中写 module "train-cluster" { source = "git::.../eks-gpu-cluster?ref=v2.1.0" instance_type = "p4d.24xlarge" node_count = 4 }。平台团队在模块内部管理好 AMI、EFA、自动扩缩容等复杂逻辑,训练团队永远看不到也不需关心这些细节。

场景二:AI 产品多环境隔离

阶段:模型服务的部署发布

解决的问题:AI 应用通常要同时跑在开发、测试、生产三套环境中,环境差异导致“在我电脑上能跑,到生产就崩”的问题。通过模块化封装网络和权限资源,三套环境使用同一模块、不同参数,实现“配置一致、环境隔离”。

具体案例:模型推理服务依赖 Redis、Kafka、对象存储等基础组件。平台组写出 ai-base-infra 模块,各环境通过传入 env = "prod" 或 env = "staging" 自动获得不同的命名前缀和容量规格,网络策略、日志采集、监控告警由模块内部统一配置。算法工程师不再需要接触任何云控制台。

Python 演示

python110 行
"""模拟 Terraform Module 仓库的解析与依赖分析工具."""import osimport re  class ModuleAnalyzer:    """扫描本地模块目录,提取变量、输出与依赖。"""     def __init__(self, root):        self.root = root        self.modules = {}        self.dep_edges = []     def _read_tf_files(self, module_path):        files = {}        for fname in ("main.tf", "variables.tf", "outputs.tf"):            fpath = os.path.join(module_path, fname)            if os.path.exists(fpath):                with open(fpath, "r", encoding="utf-8") as f:                    files[fname] = f.read()        return files     def _extract_names(self, text, pattern):        return re.findall(pattern, text)     def analyze_module(self, module_name, path):        files = self._read_tf_files(path)        variables = set()        outputs = set()        for content in files.values():            variables |= set(self._extract_names(content, r'variable\s+"([^"]+)"'))            outputs |= set(self._extract_names(content, r'output\s+"([^"]+)"'))        child_sources = self._extract_names(            files.get("main.tf", ""), r'source\s*=\s*"([^"]+)"')        self.modules[module_name] = {            "variables": sorted(variables),            "outputs": sorted(outputs),            "children": child_sources,        }        for src in child_sources:            self.dep_edges.append((module_name, src))     def analyze_repo(self):        base = self.root        for entry in sorted(os.listdir(base)):            full = os.path.join(base, entry)            if os.path.isdir(full) and any(                os.path.exists(os.path.join(full, f)) for f in                ("main.tf", "variables.tf", "outputs.tf")            ):                self.analyze_module(entry, full)        return self.modules     def print_report(self):        print("=== Terraform 模块仓库分析 ===")        for name, mod in self.modules.items():            print(f"\n模块: {name}")            print(f"  变量: {', '.join(mod['variables']) or '(无)'}")            print(f"  输出: {', '.join(mod['outputs']) or '(无)'}")            print(f"  依赖子模块: {', '.join(mod['children']) or '(无)'}")        print("\n=== 依赖关系 ===")        for parent, child in self.dep_edges:            print(f"  {parent} -> {child}")        print(f"\n共发现 {len(self.modules)} 个模块, "              f"{len(self.dep_edges)} 条依赖边。")  if __name__ == "__main__":    demo_root = os.path.join(os.path.dirname(__file__), "modules")    os.makedirs(demo_root, exist_ok=True)    os.makedirs(os.path.join(demo_root, "vpc"), exist_ok=True)     with open(os.path.join(demo_root, "vpc", "main.tf"), "w") as f:        f.write('''resource "aws_vpc" "main" {  cidr_block = var.cidr}output "vpc_id" {  value = aws_vpc.main.id}''')    with open(os.path.join(demo_root, "vpc", "variables.tf"), "w") as f:        f.write('''variable "cidr" {  type = string  default = "10.0.0.0/16"}''')     os.makedirs(os.path.join(demo_root, "eks"), exist_ok=True)    with open(os.path.join(demo_root, "eks", "main.tf"), "w") as f:        f.write('''module "vpc" {  source = "./vpc"  cidr = var.cluster_cidr}resource "aws_eks_cluster" "main" {  name = var.cluster_name}''')    with open(os.path.join(demo_root, "eks", "variables.tf"), "w") as f:        f.write('''variable "cluster_name" {  type = string}variable "cluster_cidr" {  type = string  default = "10.1.0.0/16"}''')     analyzer = ModuleAnalyzer(demo_root)    analyzer.analyze_repo()    analyzer.print_report()
目的:这个脚本模拟了一个迷你 Terraform 模块仓库,自动扫描 modules/ 目录下每个子模块的 variables.tf 和 outputs.tf,提取变量与输出名,并解析 module 块中的 source 字段生成依赖图——相当于给“菜谱库”做了一次自动索引。
  1. class ModuleAnalyzer:定义分析器类,负责扫描和解析模块目录,相当于“菜谱质检员”。
  2. self.modules = {}用字典保存每个模块的变量、输出和子模块,是分析结果的“台账”。
  3. _extract_names(content, pattern)用正则提取 variable "名字" 和 output "名字",是解析 .tf 文件的核心工具。
  4. self.dep_edges.append((module_name, src))记录一条依赖边 父模块 → 子模块,用于最终构建依赖关系清单。
  5. analyze_repo()遍历仓库根目录,对每个含 .tf 文件的子目录执行分析,相当于“挨个翻阅菜谱”。
  6. print_report()输出人类可读的分析报告,方便你直观看到模块的接口与依赖。需要验证解析结果时,直接运行脚本即可得到清晰的模块关系图。

常见误区

误区很多人觉得模块化就是把之前复制粘贴的代码存成一个文件夹,用的时候再复制一次,和原来没什么本质区别。

正解复制粘贴是“每次都用一份新副本”,副本之间会随着各自修改而分叉(配置漂移);Module 则是“所有环境引用同一份代码”,通过参数区分差异。就像菜谱只有一本,谁做都是照同一本做,而不是每人抄一份、各自改两笔,最后变成三种红烧肉。

为何常见:初学者往往只看到“代码被复用了”这个表面现象,忽略了“单点维护、源头统一”这个模块化的真正内核。

误区有人觉得模块的变量应该尽量少,最好一个都不用传,直接写死,这样调用最简单。

正解变量是模块与外部世界的“对话窗口”。该参数化的地方不参数化(如环境名、地域、实例规格),模块就只能部署到一种环境,失去了复用价值。正确的做法是面向变化设计入口:会变的量全部声明为变量并提供合理默认值,像菜谱把“口味”留成可选项,而不是把所有调料写死。

为何常见:写死参数在“自己用”时确实省事,但一旦别人用或换环境,立刻暴露问题。这是把“方便”误当成了“简单”。

误区调用模块时不写 `version` 约束,或写 `>= 0.0.0` 这种“什么都允许”的宽松条件,觉得这样能自动获得新功能。

正解不锁版本意味着模块作者下一次发布带破坏性变更的版本时,你的 terraform init 会静默拉取新版,代码直接跑崩。正确的做法是使用 ~> 限制在主版本内更新,并在升级前查看 CHANGELOG。这就像后厨规定“只能使用 2.x 代菜谱”,不能因为出了 3.0 就自动换书。

为何常见:Git 依赖默认不锁版本时,人容易高估自己的“跟进最新”能力,低估了基础设施变更的爆炸半径。

误区一些开发者喜欢写“超级模块”,把 VPC、子网、安全组、EC2、RDS 全塞进一个模块,觉得这样一步到位。

正解大模块违背了单一职责原则。任何微小的改动都可能影响所有使用者,参数组合爆炸,测试覆盖困难。专业的做法是把模块拆小:网络归网络、计算归计算、存储归存储,再通过子模块组合完成复杂场景。老王的菜谱也是“红烧肉”归红烧肉、“糖醋鱼”归糖醋鱼,而不是写一本“满汉全席”让厨师无从下手。

为何常见:规模和能力在初学者眼里常被混淆。实际上模块设计的核心是“易理解、易测试、易演进”,而不是“功能全”。

面试题

Q1. 在 Terraform 中,一个 Module 本质上是什么?

单选题 · easy

  • A.一个可以执行的二进制程序
  • B.一个包含 .tf 文件的目录← 正确答案
  • C.一条 Terraform CLI 命令
  • D.一种云服务商的特有产品

答案:B。Module 就是一组 .tf 配置文件的集合,放在一个独立目录里,通过 source 参数被其他配置加载。它并不是二进制程序、命令或云厂商功能,而是代码复用的组织方式。可以把它理解为一份写在目录里的菜谱,而不是某个做菜机器。

Q2. 下列哪个是合法的模块来源(source)?

单选题 · easy

  • A.source = "./modules/vpc"← 正确答案
  • B.source = "vpc"
  • C.source = "aws_vpc"
  • D.source = "module.vpc.id"

答案:A。source 支持本地路径、Git 仓库 URL、Terraform Registry 地址和对象存储路径。./modules/vpc 是标准的本地路径写法。其余选项都不是源码获取地址的合法格式,就像菜谱的“出处”必须是明确的地址,而不是一个菜名。

Q3. 哪个文件用于声明模块的输入变量?

单选题 · easy

  • A.main.tf
  • B.outputs.tf
  • C.variables.tf← 正确答案
  • D.versions.tf

答案:C。variables.tf 是声明 variable 块的约定文件,定义模块的入口参数;main.tf 写核心资源;outputs.tf 暴露输出;versions.tf 约束 provider 与 Terraform 版本。这就像食材清单要单独写在菜谱的固定环节里,才方便查阅。

Q4. 关于模块输出(output),下列说法正确的是?

单选题 · mid

  • A.输出只能被根模块使用
  • B.输出用于暴露模块内部资源的关键值← 正确答案
  • C.输出必须定义在 main.tf 中
  • D.输出会自动写入远程状态文件

答案:B。模块输出通过 output 块暴露内部资源属性,供调用方或其他模块引用。子模块的输出可以被根模块或兄弟模块引用。输出通常定义在 outputs.tf 中。它并不“自动”写远程状态——状态由 Terraform 自身管理。输出是“上菜时报菜名”,让外面知道里面有什么。

Q5. 版本约束 ~> 4.0 的含义是?

单选题 · mid

  • A.只允许使用 4.0.0 这一个版本
  • B.允许 4.x 任意版本,但不允许 5.0← 正确答案
  • C.允许从 0.4.0 到任意更高版本
  • D.允许 3.0 到 4.0 之间的所有版本

答案:B。~> MAJOR.MINOR 允许该主版本下所有次版本和修订版更新,但不跨主版本。所以 ~> 4.0 接受 4.1、4.9 等,但拒绝 5.0。这是一种“保持大方向不变,微调可以”的约束,如同餐厅指定“要最新一代配方,但不能换菜系”。

Q6. 子模块被多个环境引用时,如何区分不同配置?

单选题 · mid

  • A.修改模块内部代码
  • B.通过传入不同的变量值← 正确答案
  • C.复制模块到不同目录
  • D.每次 apply 前手动修改 source

答案:B。模块的价值就在于“同一份代码、不同变量”。生产、测试、预发等环境通过传入不同变量(如 CIDR、实例规格)复用同一模块,不需修改模块本身或复制代码。

Q7. 模块之间的依赖关系形成有向无环图(DAG),下列说法正确的有?

多选题 · mid

  • A.A 模块输出被 B 模块引用时,A 先创建← 正确答案
  • B.无依赖关系的模块一定串行执行
  • C.依赖图中有环不会影响 Terraform 执行
  • D.Terraform 可以并行执行无依赖的模块← 正确答案

答案:AD。A 正确:被依赖的模块必须先创建,B 才能拿到其输出。D 正确:Terraform 对无依赖关系的模块并行执行。B 错误:无依赖关系恰恰可以并行。C 错误:Terraform 禁止循环依赖。就像后厨里互不相关的菜可以同时上灶,但“先烧水才能蒸鱼”的顺序不能乱。

Q8. 要确保模块每次初始化都获取 Git 仓库中指定 tag 的代码,应如何写?

单选题 · hard

  • A.source = "github.com/org/repo"
  • B.source = "git::https://example.com/repo.git?ref=v1.2.0"← 正确答案
  • C.source = "git::https://example.com/repo.git@v1.2.0"
  • D.source = "https://example.com/repo/v1.2.0"

答案:B。Git 模块源的标准写法是 git::URL?ref=<tag或分支>,ref 参数指定 tag、分支或 commit。@ 不是 terraform 语法,裸 Git 地址默认分支,URL 直接写路径也不对。这就像点菜要明确“用第几版菜谱”——ref 就是版本标记。

Q9. 关于 Terraform Module 的最佳实践,正确的有?

多选题 · hard

  • A.模块应遵循单一职责原则← 正确答案
  • B.用 for_each 或 count 创建重复资源← 正确答案
  • C.在模块中硬编码资源 ID 以提升性能
  • D.用 examples/ 目录提供最小可用示例← 正确答案

答案:ABD。A、B、D 是社区公认的最佳实践。C 错误:硬编码资源 ID 会导致环境不可移植,应使用变量或数据源动态获取。硬编码相当于菜谱写死“只有某一家的酱油”,一旦换供应商菜就废了。

Q10. 模块内 count 为 0 时,调用方通过模块输出引用其属性会发生什么?

单选题 · extreme

  • A.输出值为空字符串
  • B.输出值为 null,属性访问会报错← 正确答案
  • C.Terraform 自动跳过所有依赖模块
  • D.输出返回 [] 空列表

答案:B。当 count = 0 时,模块创建的资源不存在,相关输出值为 null。其他模块直接引用这些属性会得到 null 并可能报错,需用 try() 或条件表达式防护。这就像菜谱上写了“如果客人没点这道菜,就别把菜端上来”,此时你也不能询问这道菜的味道。