最后审核时间:2026年5月
使用原生 Terraform 构建 004 考试中的 AWS 服务——每次构建一个代码块,并紧扣考试领域。相同的代码可在 OpenTofu 上运行。
在本实验结束时,您将完整运行核心 Terraform 工作流程,并触及所有 Associate 考试领域——无需云账户,也无需花费一分钱。我们使用无需凭证的 random 和 local 提供程序,因此重点将放在考试实际测试的内容上:Terraform 本身的行为方式。
您将编写第一个资源,使用变量和输出对其进行参数化,使用 for_each 生成多个资源,将重复部分重构为可重用模块,检查和迁移状态,最后将配置指向 HCP Terraform 进行远程运行。每个代码片段都是纯粹的 Terraform——相同的代码在 OpenTofu 上无需修改即可运行。将这些代码块放入一个 main.tf 文件中(我们将指出少数几个需要单独文件的代码块),运行一次 terraform init,然后逐步运行 terraform apply。
>= 1.5 或 OpenTofu >= 1.6 (terraform version)。我们在步骤 6 中使用了配置驱动的 import 块,这需要 1.5+ 版本。random 和 local 提供程序完全在您的机器上运行。mkdir tf-associate-lab && cd tf-associate-lab)。本实验完全免费。random 和 local 提供程序不创建任何云资源——只在您的磁盘上创建少量文件以及本地 terraform.tfstate 中的条目。步骤 7 (HCP Terraform) 使用的是免费层级,对于单个实验工作区来说绰绰有余。此处没有任何内容会在闲置时产生费用。
在运行任何内容之前,我们先声明预期的 Terraform 版本以及我们依赖的提供程序。固定版本是考试的重点之一——《Terraform 基础》和《核心工作流程》领域都测试您对 required_version、required_providers 块以及 terraform init 在将提供程序插件下载到 .terraform/ 中的作用的理解。
我们特意选择了 random 和 local。两者都不需要云登录,因此整个实验保持免费且可重现,而且考试反正也从不询问特定的云——它问的是 Terraform。将此代码放入一个新的 main.tf 文件中并运行 terraform init;您将看到 Terraform 解析并将这两个提供程序锁定到 .terraform.lock.hcl 文件中,这本身就是基础领域的一个讨论点(提交锁定文件,以便每次运行都使用相同的插件版本)。
terraform {
required_version = ">= 1.5"
required_providers {
random = {
source = "hashicorp/random"
version = "~> 3.6"
}
local = {
source = "hashicorp/local"
version = "~> 2.5"
}
}
}
# Both providers run locally and need no "provider" configuration
# block at all - a useful reminder that providers are just plugins.现在,我们将实践《使用核心 Terraform 工作流程》领域(占考试的 18%)的核心内容。我们声明一个 random_pet 资源来生成一个友好的名称,然后声明一个 local_file 资源将该名称写入磁盘。local_file.greeting 资源引用了 random_pet.name.id,正是这个引用告诉 Terraform 文件依赖于 pet——这是隐式依赖顺序,无需 depends_on。
依次运行工作流程:terraform plan 会在任何更改发生之前向您展示差异,而 terraform apply 会使更改生效并将结果记录在 terraform.tfstate 中。在不进行任何更改的情况下第二次运行 terraform apply,您将看到 No changes——这就是幂等性,考试喜欢问为什么第二次应用是空操作。有了可工作的资源和新的状态文件,我们就可以开始使配置变得灵活。
resource "random_pet" "name" {
length = 2
separator = "-"
}
resource "local_file" "greeting" {
filename = "${path.module}/hello.txt"
content = "Hello from ${random_pet.name.id}!\n"
}硬编码值对于演示来说没问题,但《读取、生成和修改配置》领域(占 19%)期望您进行参数化。我们添加一个带有 type、default 和 validation 块的输入 variable,该验证块会拒绝超出我们允许环境的任何内容——验证在计划时运行,是一个常见的考题。我们一次性计算一个 locals 映射并重用它,并使用 output 块公开结果,以便其他配置(以及 terraform output)可以读取它们。
请注意考试测试的分层:variable 是输入,locals 是派生/中间值,output 是发布结果。再次应用并尝试 terraform output pet_name 来读取单个值,或者 terraform output -json 来对其进行脚本操作。连接好输入和输出后,我们就可以停止一次编写一个资源,并生成一整套资源了。
variable "environment" {
description = "Deployment environment label."
type = string
default = "dev"
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "environment must be one of: dev, staging, prod."
}
}
locals {
common_tags = {
environment = var.environment
managed_by = "terraform"
}
}
output "pet_name" {
description = "The generated pet name."
value = random_pet.name.id
}
output "tags" {
value = local.common_tags
}实际配置很少会声明所有内容的单个实例。在这里,我们通过从集合驱动资源创建,进一步深入《读取、生成和修改配置》领域。一个 set(string) 变量列出逻辑服务;然后 for_each 为每个服务生成一个 random_string 和一个 local_file,可以分别通过 random_string.suffix["api"] 等方式寻址。
此步骤还展示了考试期望您识别的表达式和内置函数:用于当前元素的 each.key,用于构建每个服务存储桶名称的字符串插值,以及用于将 HCL 对象转换为磁盘上的 JSON 文件的 jsonencode()。我们重复使用步骤 3 中的 var.environment 和 local.common_tags,以便每个生成的文件都携带一致的元数据。下一个显而易见的问题——这变得重复了,我该如何打包它?——正是模块所要回答的。
variable "services" {
description = "Logical services to generate a config file for."
type = set(string)
default = ["api", "web", "worker"]
}
resource "random_string" "suffix" {
for_each = var.services
length = 6
special = false
upper = false
}
resource "local_file" "service_config" {
for_each = var.services
filename = "${path.module}/config/${each.key}.json"
content = jsonencode({
service = each.key
environment = var.environment
bucket = "${each.key}-${random_string.suffix[each.key].result}"
tags = local.common_tags
})
}《与 Terraform 模块交互》领域要求您编写模块、调用模块并传入传出值。我们将每个服务的逻辑移到 ./modules/service 下的子模块中,为其提供自己的 variable 输入和 output,然后从根模块中用 for_each 调用它——每个服务一个模块实例。
下面这两个文件清晰地展示了边界:子模块不知道哪些服务存在(那是调用者通过 var.name 的工作),而根模块也不知道如何构建服务(这被封装在模块中)。添加模块后再次运行 terraform init——考试会测试新的模块源需要重新初始化才能安装。现在我们的配置已经模块化,Associate 考试的最后一个大主题是 Terraform 一直在悄悄跟踪的内容:状态。
# modules/service/main.tf
variable "name" {
type = string
}
variable "environment" {
type = string
}
resource "random_string" "suffix" {
length = 6
special = false
upper = false
}
resource "local_file" "config" {
filename = "${path.root}/config/${var.name}.json"
content = jsonencode({
service = var.name
environment = var.environment
bucket = "${var.name}-${random_string.suffix.result}"
})
}
output "bucket_name" {
value = "${var.name}-${random_string.suffix.result}"
}
# main.tf (root) - call the module once per service
module "service" {
source = "./modules/service"
for_each = var.services
name = each.key
environment = var.environment
}
output "service_buckets" {
value = { for k, m in module.service : k => m.bucket_name }
}《实施和维护状态》(占 19%)和《在核心工作流程之外使用 Terraform》(占 9%)领域都在此体现。状态是您配置地址到实际对象的 JSON 账本;terraform state list 枚举它,terraform state show <addr> 打印一个条目。考试期望您知道,在配置中重命名资源通常会销毁并重新创建它——除非您告诉 Terraform 地址已移动。
moved 块正是以声明方式执行此操作:将 local_file.greeting(来自步骤 2)重命名为 local_file.welcome,moved 块会就地迁移状态,因此 plan 显示的是移动,而不是销毁和重新创建。(命令式等效项是 terraform state mv local_file.greeting local_file.welcome。)我们还展示了一个配置驱动的 import 块——这是 1.5+ 版本将预先存在的对象导入状态而无需旧的 terraform import CLI 命令的方式。在状态得到控制后,还剩下一种功能需要满足:远程运行所有这些。
# Renaming a resource? A "moved" block migrates state in place
# instead of destroying and recreating the object. Replace the
# Step 2 "greeting" resource with this renamed "welcome" one.
moved {
from = local_file.greeting
to = local_file.welcome
}
resource "local_file" "welcome" {
filename = "${path.module}/hello.txt"
content = "Hello from ${random_pet.name.id}!\n"
}
# Config-driven import (Terraform 1.5+): adopt an object that
# already exists into state, no "terraform import" CLI command.
# terraform_data is a built-in resource - nothing to provision.
import {
to = terraform_data.tracked
id = "existing-id"
}
resource "terraform_data" "tracked" {}最后一个领域,《理解 HCP Terraform 功能》(占 5%),完善了考试内容。terraform {} 内部的单个 cloud 块将本地执行替换为 HCP Terraform:状态远程存储并在运行期间锁定,terraform plan/apply 在 HashiCorp 的运行器上执行,并且运行输出(以及存储的计划)显示在 Web UI 中。这也是考试将 HCP Terraform 功能(远程状态、运行历史、使用 Sentinel/OPA 进行策略强制执行以及私有模块注册表)与我们在步骤 1-6 中使用的纯本地工作流程进行对比的地方。
将 my-org 替换为您自己的组织,运行一次 terraform login 以存储 API 令牌,然后运行 terraform init 将状态迁移到远程工作区。您在此实验中编写的所有内容现在都运行不变——只有运行位置发生了变化。从一个裸露的 main.tf 到一个远程支持的工作区,这一整个过程就是 Associate 认证的完整历程。
terraform {
cloud {
organization = "my-org"
workspaces {
name = "terraform-associate-lab"
}
}
}
# Run once to authenticate, then re-init to migrate state:
# terraform login
# terraform init所有内容都存在于您的机器上,因此清理很快:
terraform destroy 以删除生成的文件并从状态中清除它们。terraform-associate-lab 工作区(或者先对其运行 terraform destroy),然后移除 cloud 块。cd .. && rm -rf tf-associate-lab。本地的 .terraform/ 插件缓存、.terraform.lock.hcl 和 terraform.tfstate 都会随之删除。Associate 考试是关于 Terraform 工具的,而不是任何特定的云,因此本实验特意不预置任何云基础设施。我们故意跳过 AWS / Azure / GCP 资源:它们需要凭证,可能会产生费用,并且会分散对考试实际测试机制的注意力——即工作流程、状态、配置语言和模块。
有些 Associate 主题最好是阅读而非实践:除 HCP Terraform 之外的后端(S3、Azure Blob、GCS、Consul),remote-exec 等配置器(HashiCorp 将其列为最后手段),以及用于管理多个状态实例的工作区。对于这些,此认证页面的浏览和手册部分提供了概念性内容。这里的动手价值是 init → plan → apply 的肌肉记忆、读取状态文件以及使用 moved 安全地重构。