最終確認: 2026年5月
TF-PRO 試験の対象となる AWS サービスを、プレーンな Terraform を使用して構築します。1 ブロックずつ、それぞれ試験ドメインに関連付けられています。同じコードが OpenTofu でも動作します。
このラボを完了すると、Terraform Pro試験で最も重視される4つのドメインを習得できます。具体的には、高度な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) は無料ティアで実行され、単一のラボワークスペースと試験で問われる運用機能がカバーされます。
HCLと構成のドメインを開始するにあたり、まず最新のTerraformを固定します。Pro試験では、新しいリリースにのみ存在する言語機能(optional()オブジェクト属性、removedブロック)が想定されています。required_providersブロックはrandomとlocalをロックし、terraform initは.terraform.lock.hclを書き込みます。このファイルは、チーム全体でプランが再現可能になるようにコミットすべきです。
今身につけるべき2つの習慣は、ラボ全体を通じて重要です。terraform fmtはスペースとアライメントを標準化し(試験では、ファイルがその場で書き換えられ、-checkモードでゼロ以外の終了コードを返すことが問われます)、terraform validateは、プロバイダーに接続する前に構成が内部的に整合しているかをチェックします。ツールチェーンが固定され、これらの2つのコマンドが手元にあれば、検証する価値のある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、ネストレベルを1つ下げる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の下に、独自のrequired_providers、検証された入力、および公開された出力を持つworkload子モジュールを抽出します。名前の検証における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を反復処理することは、試験で1つのモジュールの出力を別のリソースの入力に連携させる標準的な方法です。
集約ファイルに対する明示的な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試験と区別される部分です。ここでは、設定駆動型のステート操作を3つ使用します。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ブロック — ここでは1つの構成が多くのタグに一致するワークスペースにマッピングできるようworkspaces { tags = [...] }を使用 — により、実行はノートPCから移動します。実行はリモートでロックされたステートを持つHCP Terraform上で行われます。試験では、リモート、ローカル、エージェントの実行モードと、VCS駆動、CLI駆動、API駆動の実行ワークフローを対比させます。
また、Pro試験で挙げられる運用機能がここに集約されています。ワークスペース間で入力を共有する変数セット、あるワークスペースのapplyを別のワークスペースのplanに連鎖させる実行トリガー、外部統合のための実行タスク、applyを制御するポリシーセット(SentinelまたはOPA)、そしてステップ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ポリシーのオーサリング、およびプライベートモジュールレジストリの公開フローです。この認定ページの閲覧タブとプレイブックタブでは、これらの概念がカバーされています。ここでのハンズオンの価値は、検証された複雑な入力、構成されたモジュール、恐れることのないステート操作、およびリモート実行といった、試験の根幹をなすオーサリングとオペレーションのループにあります。