設定ファイルの読み込みと式の評価:HCLから cty.Value へ
前提知識
- ›第1回:アーキテクチャとコードベースの読み方
- ›第2回:グラフエンジンと DAG ウォーク(評価タイミングの理解に必要)
- ›HCL 構文の基本的な知識
設定ファイルの読み込みと式の評価:HCL から cty.Value へ
このシリーズでは、グラフの構築やウォークが始まる前に設定がすでに存在しているものとして話を進めてきました。しかし実際には、ディスク上の .tf ファイルからプロバイダー呼び出しを駆動する cty.Value が得られるまでには、洗練されたパイプラインが存在します。そのパイプラインは、仮想ファイルシステム抽象化を経由した HCL パース、モジュールツリーの組み立て、豊富な Go 型モデルによる設定表現から構成されています。そして何より重要なのは、パース時ではなくグラフウォーク中に行われる式の遅延評価です。
この遅延評価こそが、Terraform の依存関係システムを支える仕組みです。リソースの設定はすぐに AST としてパースされますが、その設定内の式が持つ値は、グラフウォークがそのリソースのノードに到達するまで解決されません。そのタイミングまでには、上流の依存関係がすべて評価済みになっています。
Parser:HCL と仮想ファイルシステム
設定の読み込みは、internal/configs/parser.go#L20-L30 にある Parser から始まります。
type Parser struct {
fs afero.Afero
p *hclparse.Parser
allowExperiments bool
}
afero.Afero という仮想ファイルシステム抽象化を採用しているのは意図的な設計です。これにより、インメモリファイルシステムを使ったテスト、アーカイブからの設定読み込み(プランファイルには設定が埋め込まれています)、そして将来的なリモート設定ソースへの対応——これらすべてをパースロジックを変えることなく実現できます。
flowchart TD
FS["Filesystem (afero)"] -->|"ReadFile"| Parser["configs.Parser"]
Parser -->|".tf files"| HCLNative["hclparse.ParseHCL"]
Parser -->|".tf.json files"| HCLJSON["hclparse.ParseJSON"]
HCLNative --> Body["hcl.Body"]
HCLJSON --> Body
Body --> Decode["Decode into Go structs"]
Decode --> File["configs.File"]
File -->|"multiple files"| Module["configs.Module"]
実際の HCL パースは hclparse.Parser に委譲されます。59〜80行目 の LoadHCLFile メソッドは、ファイルの拡張子に基づいてフォーマットを判別します。.json ファイルは HCL の JSON 構文として、それ以外はネイティブの HCL 構文として処理されます。Terraform がネイティブ HCL と JSON のどちらでも設定を書ける理由はここにあります。パーサーが両方を同じ hcl.Body 表現に正規化するのです。
allowExperiments フラグは、試験的な言語機能へのアクセスを制御します。アルファリリースのビルド時には true に設定され、for_each のモジュール対応(正式リリース前)や新機能が利用可能になります。このフラグはパース時と使用時の両方でチェックされており、多層的な防御を実現しています。
ヒント: パーサーは
hclparse.Parserを通じて読み込んだファイルをすべて内部キャッシュに保持しています。このキャッシュは後にエラーメッセージのソースコードスニペット生成に使われます。エラーメッセージにソースコードの抜粋が表示されるのは、このキャッシュのおかげです。
Module モデル:単一ディレクトリの内容
個々のファイルをパースした後、それらは internal/configs/module.go#L19-L63 の Module にマージされます。
type Module struct {
SourceDir string
CoreVersionConstraints []VersionConstraint
ActiveExperiments experiments.Set
Backend *Backend
StateStore *StateStore
CloudConfig *CloudConfig
ProviderConfigs map[string]*Provider
ProviderRequirements *RequiredProviders
Variables map[string]*Variable
Locals map[string]*Local
Outputs map[string]*Output
ModuleCalls map[string]*ModuleCall
ManagedResources map[string]*Resource
DataResources map[string]*Resource
EphemeralResources map[string]*Resource
ListResources map[string]*Resource
Actions map[string]*Action
Moved []*Moved
Removed []*Removed
Import []*Import
Checks map[string]*Check
Tests map[string]*TestFile
}
classDiagram
class Module {
+SourceDir string
+Backend *Backend
+ProviderConfigs map[string]*Provider
+Variables map[string]*Variable
+Locals map[string]*Local
+Outputs map[string]*Output
+ModuleCalls map[string]*ModuleCall
+ManagedResources map[string]*Resource
+DataResources map[string]*Resource
+EphemeralResources map[string]*Resource
+ListResources map[string]*Resource
+Actions map[string]*Action
+Moved []*Moved
+Removed []*Removed
+Import []*Import
}
class Resource {
+Mode ResourceMode
+Name string
+Type string
+Config hcl.Body
+Count hcl.Expression
+ForEach hcl.Expression
+ProviderConfigRef *ProviderConfigRef
+DependsOn []hcl.Traversal
}
class Variable {
+Name string
+Type cty.Type
+Default cty.Value
+Validation []*CheckRule
+Ephemeral bool
}
Module --> Resource
Module --> Variable
Module は単一ディレクトリの内容を表します。同じディレクトリ内のすべての .tf ファイルが一つの Module にマージされます。複数のファイルにリソースを分散して書いても互いに参照し合えるのはこのためです。
注目すべきは、Resource.Config が デコード済みの Go 構造体ではなく hcl.Body であるという点です。これが遅延評価の鍵で、リソースの属性値はこの段階では未評価の HCL 式のまま保持されます。実際に評価されるのはグラフウォーク中——つまり、参照している変数や他リソースの値が確定したタイミングです。
リソースが ManagedResources、DataResources、EphemeralResources、ListResources に分かれているのは、Terraform が現在サポートする4つのリソースモードを反映しています。それぞれライフサイクルのセマンティクスは異なりますが、設定の型としては同じ Resource を共有しています。
Config ツリー:モジュール階層の組み立て
個々の Module は、internal/configs/config.go#L32-L97 にある Config 型を通じてツリーとして組み立てられます。
type Config struct {
Root *Config
Parent *Config
Path addrs.Module
Children map[string]*Config
Module *Module
// source info, version constraints...
}
classDiagram
class Config {
+Root *Config
+Parent *Config
+Path addrs.Module
+Children map[string]*Config
+Module *Module
+Version *version.Version
}
Config --> Config : Root, Parent
Config --> Config : Children[name]
Config --> Module : Module
ツリーの組み立てを担うのは config_build.go です。ルートモジュールの module 呼び出しを処理し、ローカルパス、レジストリ、その他のソースから子モジュールを再帰的に読み込みます。Root フィールドは常にルートの Config を指し、Parent は呼び出し元モジュールを指します。Path フィールド(型は addrs.Module)は静的なモジュールパスを保持します——例えば module.network.module.vpc のような形式です。
このツリー構造は静的なものです。設定として宣言されたモジュール階層を表しており、グラフウォーク時に count や for_each によって複数のインスタンスへ展開されると、動的な addrs.ModuleInstance パスが生成されます。この展開を担うのが、第2回で紹介した ModuleExpansionTransformer です。
アドレスシステム
addrs パッケージはコードベース全体の命名基盤を提供しています。プロバイダー、モジュール、リソース、変数、出力——Terraform のアドレス指定可能なオブジェクトにはそれぞれ対応する型があります。
| 型 | 例 | 用途 |
|---|---|---|
addrs.Provider |
registry.terraform.io/hashicorp/aws |
プロバイダーの識別と FQN |
addrs.Module |
module.network.module.vpc |
静的なモジュールパス |
addrs.ModuleInstance |
module.network[0].module.vpc |
キーを含む動的なモジュールインスタンス |
addrs.Resource |
aws_instance.web |
モジュール内のリソース |
addrs.ResourceInstance |
aws_instance.web[0] |
count/for_each キーを持つインスタンス |
addrs.AbsResourceInstance |
module.network.aws_instance.web[0] |
完全修飾インスタンス |
addrs.Reference |
任意のアドレス可能オブジェクトへの参照 | ReferenceTransformer で使用 |
internal/addrs/provider.go#L15 にある addrs.Provider 型は、外部パッケージ terraform-registry-address の tfaddr.Provider のエイリアスです。
type Provider = tfaddr.Provider
これらのアドレス型は、状態やグラフノードレジストリのマップキーとして、-target フラグのターゲット指定として、そして参照解決の基盤として使われます。人間が読める文字列への変換には String() が実装されており、等値比較もサポートしているためマップキーに適しています。
ヒント: Terraform のソースコードを読む際は、関数が受け取るのが
addrs.Resource(静的、インスタンスキーなし)なのかaddrs.AbsResourceInstance(モジュールパスとインスタンスキーを含む完全修飾)なのかに注意してください。アドレスのレベルを誤って渡すことは、バグの典型的な原因の一つです。
遅延式評価:lang.Scope と cty
Terraform の設定システムで最も重要な概念は、式はパース時に評価されないということです。リソースの設定、変数のデフォルト値、出力値などに格納されている hcl.Expression は、グラフウォーク中に lang.Scope 型を通じて遅延評価されます。
評価のパイプラインは次のようになっています。
sequenceDiagram
participant Parser as configs.Parser
participant Module as configs.Module
participant Graph as PlanGraphBuilder
participant Walk as Graph Walk
participant Node as ResourceNode
participant Scope as lang.Scope
participant Provider as Provider
Parser->>Module: Parse .tf files
Note over Module: Stores hcl.Expression<br/>(unevaluated AST)
Module->>Graph: Config input
Graph->>Walk: Built graph
Walk->>Node: Execute(evalCtx)
Node->>Scope: EvalBlock(config, schema)
Scope->>Scope: Resolve references<br/>(var.x, local.y, aws_vpc.main.id)
Scope-->>Node: cty.Value (resolved)
Node->>Provider: PlanResourceChange(resolvedConfig)
NodePlannableResourceInstance のようなグラフノードが実行されると、プロバイダーに送る「変更案の新状態」を生成するためにリソースの設定を評価する必要があります。この処理は EvalContext を通じて行われ、以下の情報を持つ lang.Scope にアクセスします。
- 入力変数の値 —
PlanOpts.SetVariablesまたはデフォルト値から解決 - ローカル値 — それぞれの式から計算
- 子モジュールの出力値 — そのモジュールの処理完了後に利用可能
- リソースの属性 — 対応するリソースのノード完了後に利用可能
- 組み込み関数 —
length()、lookup()、format()など
internal/terraform/eval_variable.go での変数評価と、internal/terraform/evaluate_data.go でのデータ評価を見ると、この仕組みが具体的にどう動くかがわかります。重要なのは、グラフの依存エッジが評価順序を保証しているという点です。リソース B がリソース A を参照している場合、ReferenceTransformer が A のノードを B のノードより先に完了させます。B が式を評価するタイミングには、A の属性値がすでにスコープに存在しているわけです。
cty(「シーティ」と読みます)型システムは、Terraform の動的な値表現を提供する外部ライブラリです。Go の静的型システムとは異なり、cty.Value は文字列、数値、真偽値、リスト、マップ、オブジェクト、セットといった Terraform のあらゆる型を表現できます。さらに、cty.UnknownVal(apply 後にしか確定しない計算済み属性を表す)や cty.NullVal(値が存在しないことを表す)といった特殊な値もサポートしています。
特に興味深いのが Unknown 値です。プランニング中、多くのリソース属性は unknown のままになります。たとえば EC2 インスタンスの ID は apply 後にしか確定しません。Terraform はこの unknown を式評価を通じて伝播させます。別のリソースの設定で aws_instance.web.id を参照しており、その ID が unknown の場合、下流の式も unknown として評価されます。これにより、まだ作成されていないインフラに依存するリソースもプランできるのです。
まとめ
7本の記事を通じて、Terraform のアーキテクチャを外側のシェルから内側のコアまで追いかけてきました。
- 第1回 ではコードベース全体を俯瞰しました:起動シーケンス、コマンド登録、パッケージ構成、エンドツーエンドのリクエストフロー。
- 第2回 ではグラフエンジンを解剖しました:汎用 DAG ライブラリ、並列ウォーカー、Transformer パイプライン。
- 第3回 では一つのリソースがプランとアプライのライフサイクルをたどる過程を追いました:Context のオーケストレーション、ノードの実行、フックシステム。
- 第4回 ではプロバイダープラグインシステムを探索しました:gRPC 通信、プロトコルバージョン、3層の抽象化。
- 第5回 では状態とバックエンドを詳しく見ました:3層の状態モデル、バックエンドインターフェースの階層、マイグレーションフロー。
- 第6回 では CLI レイヤーを解説しました:コマンド構造、人間向け/JSON 向け出力の View、診断システム。
- 本記事 では設定の読み込みと遅延式評価によって全体像を締めくくりました。
これらすべてをつなぐ共通の軸はグラフです。設定はモジュールツリーとしてパースされ、グラフの構築に供給されます。グラフの頂点であるリソースノードは、ウォーク時に設定の式を遅延評価してプロバイダーを呼び出します。その結果は状態と変更セットに流れ込みます。CLI レイヤーはフックを通じて実行を観察し、View を通じて出力をレンダリングします。バックエンドが状態を永続化し、診断システムがあらゆるレイヤーを越えて豊富なエラー情報を運びます。
10年を超えるプロジェクトでありながら、Terraform のアーキテクチャは際立って一貫性を保っています。providers.Interface、GraphTransformer、EvalContext、statemgr.Full、tfdiags.Diagnostics といった抽象化は、明確に定義されたコントラクトのセットを形成しています。これらがコアエンジンの複雑さをコントロールしながら、Terraform を取り巻く巨大なエコシステムを支えています。このコントラクトを理解することが、このコードベースを効果的に扱うための鍵です。