最后审核时间:2026年5月
使用原生 Terraform 构建 TF-PRO 考试中的 AWS 服务——每次构建一个代码块,并紧扣考试领域。相同的代码可在 OpenTofu 上运行。
完成本实验后,您将掌握 Terraform Pro 考试最侧重的四个领域:编写高级 HCL、构建和组合模块、执行状态操作以及对 HCP Terraform 进行操作——所有这些都将使用无需凭证的 random 和 local 提供程序,因此无需支付任何费用,只需清理一个文件夹即可。
您将使用丰富的类型约束和验证来建模输入,使用考试所依赖的表达式和函数转换集合,编写可重用模块并使用 for_each 展开它,然后使用 import、moved 和 removed 块重构实时状态。最后一步将整个配置连接到 HCP Terraform 进行远程执行。每个代码片段都是纯粹的 Terraform——在 OpenTofu 上完全相同。在一个工作目录中构建它;我们指出位于 ./modules 下的文件。
>= 1.9 或 OpenTofu >= 1.8 (terraform version)。我们使用 removed 块 (1.7+) 和 import 块 (1.5+),因此当前版本在这里比在 Associate 实验中更重要。random 和 local 完全在您的机器上运行。mkdir tf-pro-lab && cd tf-pro-lab)。terraform fmt 和 terraform validate 的习惯——Pro 考试假定您将它们视为编写的一部分,而不是事后才考虑。本实验完全免费。random 和 local 提供程序仅在您的磁盘上创建小文件和一个本地 terraform.tfstate;没有在任何云中配置任何内容,并且在空闲时不会产生任何费用。步骤 7 (HCP Terraform) 在免费层上运行,涵盖单个实验工作区和考试所需的操作功能。
我们通过固定最新 Terraform 版本来开始 HCL 和配置 领域——Pro 考试假设的语言功能只存在于较新的版本中 (optional() 对象属性、removed 块)。required_providers 块会锁定 random 和 local,而 terraform init 会写入一个 .terraform.lock.hcl 文件,您应该提交该文件,以便计划在团队中可重现。
现在要养成的两个习惯将贯穿整个实验:terraform fmt 会规范间距和对齐(考试会测试它是否会就地重写文件并在 -check 模式下以非零退出),而 terraform validate 会在联系任何提供程序之前检查您的配置是否内部一致。随着工具链的固定和这两个命令的掌握,我们可以编写值得验证的 HCL。
terraform {
required_version = ">= 1.9"
required_providers {
random = {
source = "hashicorp/random"
version = "~> 3.6"
}
local = {
source = "hashicorp/local"
version = "~> 2.5"
}
}
}
# Build the muscle memory:
# terraform fmt # rewrite files to canonical style
# terraform validate # check internal consistency, offlinePro 考试远不止 type = string。在这里,我们声明一个带有 optional() 属性的 object 类型,当调用者省略这些属性时,它们会提供默认值,然后附加多个 validation 块——考试会测试一个变量是否可以承载多个验证块,每个块都有自己的 condition 和 error_message。
locals 中的 try() 是另一个 Pro 级别操作:它返回第一个在评估时没有错误的表达式,因此读取可能不存在的属性会优雅地降级,而不是中止计划。它的兄弟 can()(返回布尔值)将在步骤 4 的验证中出现。这个 platform 变量成为本实验其余部分的单一事实来源——每个后续步骤都将从中读取,因此在这里正确获取类型及其保证将在下游获得回报。
variable "platform" {
description = "Platform configuration."
type = object({
name = string
replicas = optional(number, 2)
features = optional(set(string), [])
owners = list(string)
})
validation {
condition = var.platform.replicas >= 1 && var.platform.replicas <= 10
error_message = "replicas must be between 1 and 10."
}
validation {
condition = length(var.platform.owners) > 0
error_message = "At least one owner is required."
}
default = {
name = "lab"
owners = ["platform@example.com"]
}
}
locals {
# try() returns the first error-free expression - here it shields
# against features being unset and classifies the tier.
tier = length(try(var.platform.features, [])) > 0 ? "enhanced" : "standard"
}集合操作是 HCL 和配置 领域中最密集的。我们使用 setproduct() 构建每个区域/服务对,然后使用 for 表达式将这些对重塑为带键的映射——这正是后续 for_each 的输入模式。与此同时,flatten() + distinct() 将所有者列表的列表折叠成一个干净的唯一集合。
这些是 Pro 考题反复出现的函数:带有 => 的 for 用于生成映射,setproduct 用于笛卡尔积,flatten 用于降低一层嵌套,merge 用于合并映射,以及 jsonencode 用于序列化结果。我们将计算出的 deployments 映射渲染到一个文件中,以便您可以 cat out/deployments.json 并查看您的表达式生成的形状。这个带键的映射正是我们接下来要传递给模块 for_each 的值。
locals {
regions = ["us-east-1", "eu-west-1"]
services = ["api", "web"]
# setproduct builds every (region, service) pair; the for
# expression reshapes the pairs into a keyed map.
deployments = {
for pair in setproduct(local.regions, local.services) :
"${pair[0]}/${pair[1]}" => {
region = pair[0]
service = pair[1]
}
}
# flatten + distinct collapse nested owner lists into a unique set.
all_owners = distinct(flatten([
for owner in var.platform.owners : split(",", owner)
]))
}
resource "local_file" "matrix" {
filename = "${path.module}/out/deployments.json"
content = jsonencode(local.deployments)
}现在进入 模块 领域 (25%)。我们在 ./modules/workload 下提取一个 workload 子模块,它有自己的 required_providers、经验证的输入和已发布的输出。名称验证中的 can(regex(...)) 是 Pro 习惯用法:regex 会在不匹配时抛出错误,而 can 会将该错误转换为 condition 可以使用的 false。
一个精心构建的模块会隐藏其内部细节并暴露一个稳定的契约——调用者传入 name 和 replicas,并获得 id 和 manifest_path,而不会触及内部的 random_string。模块声明其自己的提供程序要求是一个明确的 Pro 考点:提供程序配置默认从根继承,但提供程序要求是按模块声明的。有了一个干净的模块,有趣的问题是如何多次实例化它并将这些实例连接起来。
# modules/workload/main.tf
terraform {
required_providers {
random = {
source = "hashicorp/random"
version = "~> 3.6"
}
local = {
source = "hashicorp/local"
version = "~> 2.5"
}
}
}
variable "name" {
type = string
validation {
# can() turns regex's raise-on-no-match into a usable bool.
condition = can(regex("^[a-z][a-z0-9-]{1,30}$", var.name))
error_message = "name must be 2-31 chars: lowercase letter, then letters/digits/hyphens."
}
}
variable "replicas" {
type = number
default = 1
}
resource "random_string" "suffix" {
length = 6
special = false
upper = false
}
resource "local_file" "manifest" {
filename = "${path.root}/out/${var.name}.json"
content = jsonencode({
name = var.name
replicas = var.replicas
id = "${var.name}-${random_string.suffix.result}"
})
}
output "id" {
description = "Stable identifier for this workload."
value = "${var.name}-${random_string.suffix.result}"
}
output "manifest_path" {
value = local_file.manifest.filename
}仍然在 模块 领域,我们现在通过对一个集合使用 for_each,为每个工作负载调用一次 module.workload,根据每个键有条件地设置 replicas,然后将每个实例的 id 输出聚合到一个单独的 registry.json 中。使用 for 表达式迭代 module.workload 是考试中将一个模块的输出连接到另一个资源的输入的标准方式。
聚合文件上的显式 depends_on 是一个教学点:module.workload 引用的隐式依赖已经正确地安排了顺序,但 depends_on 使意图一目了然,并且在依赖没有通过数据表达时偶尔需要。添加模块源后运行 terraform init,然后 apply。有了几个活动的模块实例及其聚合的输出,我们就有了一个值得操作的真实状态——这就是下一个领域。
# main.tf (root) - fan the module out, then aggregate its outputs
module "workload" {
source = "./modules/workload"
for_each = toset(["api", "web", "worker"])
name = each.key
replicas = each.key == "api" ? 3 : 1
}
resource "local_file" "registry" {
filename = "${path.module}/out/registry.json"
content = jsonencode({
for k, m in module.workload : k => m.id
})
depends_on = [module.workload]
}
output "workload_ids" {
value = { for k, m in module.workload : k => m.id }
}CLI 和状态管理 领域 (25%) 是 Pro 考试与 Associate 考试的区别所在。我们使用三种配置驱动的状态操作。import 块将一个已存在的对象引入状态——我们针对内置的 terraform_data,因此示例无需凭证。removed 块会从状态中删除资源,但不销毁实际对象——这正是您将所有权移交给另一个配置时所需要的。而带有 replace_triggered_by 的 terraform_data 会在步骤 5 的注册表更改时强制下游替换,不涉及任何提供程序。
同时也要了解考试仍在测试的命令式 CLI 等效项:terraform state list 和 state show 用于检查,terraform state mv 用于重命名,terraform state rm 用于遗忘,terraform plan -target=ADDR 用于缩小运行范围,以及 terraform apply -replace=ADDR 用于强制重新创建资源。在本地牢牢控制状态后,最后一个领域是在 HCP Terraform 上完成所有这些操作。
# 1) Adopt an existing object into state (Terraform 1.5+),
# replacing the older imperative "terraform import" command.
import {
to = terraform_data.legacy
id = "existing-id"
}
resource "terraform_data" "legacy" {}
# 2) Stop managing a resource WITHOUT destroying it (Terraform 1.7+).
removed {
from = random_string.deprecated
lifecycle {
destroy = false
}
}
# 3) terraform_data + replace_triggered_by recreates a marker
# whenever the registry file content changes.
resource "terraform_data" "deploy_marker" {
input = local_file.registry.content
lifecycle {
replace_triggered_by = [local_file.registry]
}
}HCP Terraform 操作 领域 (20%) 结束了考试。cloud 块——这里使用 workspaces { tags = [...] },因此一个配置可以映射到多个标签匹配的工作区——将执行从您的笔记本电脑转移。运行发生在 HCP Terraform 上,具有远程、锁定的状态;考试对比了 remote、local 和 agent 执行模式以及 VCS-driven、CLI-driven 和 API-driven 运行工作流。
这也是 Pro 考试中提到的操作功能汇集之处:variable sets 用于在工作区之间共享输入,run triggers 用于将一个工作区的应用链接到另一个的计划,run tasks 用于外部集成,policy sets (Sentinel 或 OPA) 用于门控应用,以及 private module registry 用于发布您在步骤 4 中构建的那种模块。将 my-org 替换为您的组织名称,运行 terraform login,然后 terraform init 以迁移状态。您在步骤 1-6 中编写的配置运行不变——只有执行地点及其周围的防护措施得到了升级,这就是整个 Pro 故事。
terraform {
cloud {
organization = "my-org"
workspaces {
tags = ["terraform-pro-lab"]
}
}
}
# Authenticate once, then migrate state to the remote workspace:
# terraform login
# terraform init一切都在本地,所以拆解很快:
terraform destroy 以删除生成的文件并从状态中清除它们。由于步骤 6 中的 removed 块,random_string.deprecated(如果曾经存在)会被遗忘而不是销毁——这是预期行为。terraform destroy,然后在 UI 中删除 terraform-pro-lab 工作区并移除 cloud 块。cd .. && rm -rf tf-pro-lab。.terraform/ 缓存、.terraform.lock.hcl 以及任何本地状态都将随之删除。Pro 考试测试的是 Terraform 的编写和操作,而不是特定的云,因此本实验故意不提供任何云基础设施——这使其无需凭证,并让每个步骤都专注于 HCL、模块、状态和 HCP Terraform。
少数与 Pro 相关的议题更适合学习而非在此处进行配置:dynamic 块(需要一个具有可重复嵌套块的资源类型——即云资源——才能有意义),提供程序 configuration_aliases 以及将别名提供程序传递到模块中,Sentinel / OPA 策略编写,以及私有模块注册表发布流程。本认证页面的浏览和手册部分从概念上涵盖了这些内容。这里的动手价值是考试所围绕的编写和操作循环:经过验证的复杂输入、组合模块、无畏的状态操作和远程运行。