「開発・本番の切り替えでファイルを書き換えたら、うっかり本番用の値で開発環境を壊してしまった」
「モジュールを分割し始めたら、どの値をvariableにして、どの値をoutputで公開するべきか整理できなくなった」
Terraformを触り始めたころ、こういった失敗は誰もが経験します。その根本原因は、Terraformの変数機能を正しく使っていないことにあります。
TerraformのHCLには「variable」「locals」「output」「data source」という4種類の変数・参照機能があります。これらを使い分けることで、コードの重複を排除し、環境ごとの値切り替えをファイルコピーなしに実現できます。さらに、Terraform 1.2以降で使えるprecondition・postconditionを組み合わせれば、設定ミスをapplyの前後に多段で検知できます。
この記事では、HCL変数設計の4要素を実際のコードと実行例で解説します。variables.tfの型定義・バリデーション・tfvarsによる環境分離・locals/outputの使い所・data sourceによる既存リソース参照、そしてlifecycleブロックのprecondition/postconditionによる多段防御まで、設計軸で整理して体得できます。
実行環境:Terraform 1.8.x(RHEL 9.4 / Ubuntu 24.04 LTS で動作確認済み)
この記事のポイント
・variable{}で入力値を外部化し、terraform.tfvarsで環境ごとに安全に切り替えられる
・locals{}は繰り返し使う式や計算結果を1箇所にまとめてDRYにするための要素
・output{}はモジュールから外部へ値を公開し、モジュール間の疎結合を保つ
・data{}ソースは「Terraformで作っていない既存リソース」をコードから参照するための機能
・precondition・postconditionはvariable validationで届かない実行時コンテキスト条件を多段で検証する
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
なぜ変数設計がTerraformコードの品質を決めるのか
変数設計を怠るとどうなるか、まず具体例で確認します。# 変数を使わないHCLの例 -- ハードコードの問題 resource "aws_instance" "web" { ami = "ami-0abcdef1234567890" instance_type = "t3.micro" subnet_id = "subnet-0abcdef1234" tags = { Name = "dev-web" Env = "dev" } } resource "aws_instance" "app" { ami = "ami-0abcdef1234567890" # 同じ値が重複 instance_type = "t3.micro" # 同じ値が重複 subnet_id = "subnet-0abcdef1234" # 同じ値が重複 tags = { Name = "dev-app" Env = "dev" } }
Terraformが提供するvariable・locals・output・data sourceの4要素を使いこなすことで、これらを解消できます。4つの役割を一言でまとめると「variableで外から受け取り、localsで内部で整理し、outputで外に渡し、dataで外の既存リソースを参照する」です。1つずつ見ていきましょう。
variable{}で入力変数を定義する
variable{}は、HCLコードの外から値を渡すための入力変数を定義します。.tfvarsファイル・コマンドラインオプション・環境変数から値を注入できます。1. 基本的な書き方
variables.tfというファイルにまとめて定義するのが一般的です。# variables.tf variable "aws_region" { description = "AWSリージョン" type = string default = "ap-northeast-1" } variable "env" { description = "環境名(dev/stg/prd)" type = string # defaultなし -- 必ず外部から指定する必要がある } variable "instance_type" { description = "EC2インスタンスタイプ" type = string default = "t3.micro" }
var.変数名という形式で参照します。# main.tf resource "aws_instance" "web" { ami = var.ami_id instance_type = var.instance_type tags = { Name = "${var.env}-web" Env = var.env } }
2. 必須入力値とデフォルトあり入力値を明確に分ける
variableの設計で最初に決めるべき判断は「defaultを設けるかどうか」です。モジュール化した構成では、この分け方が呼び出し元の使いやすさを左右します。・必須入力値(default なし):呼び出し元が必ず指定しなければならない値。サブネットIDやVPC IDなどリソース固有の値が該当します。defaultを省略するとplanの段階でエラーになり、指定漏れを防止できます。
・デフォルトあり入力値(default あり):標準的な値を提供しつつ、必要に応じて上書きできる値。インスタンスタイプや名前プレフィックスなど「大半のケースで共通する値」に使います。
設計の鉄則として「迷ったらdefaultなし(必須)にする」のが安全側です。defaultを設けると呼び出し元がその値に気づかないまま使い続けるリスクがあります。逆に、defaultがなければplanで必ずエラーが出るため、設計の意図が自然に伝わります。
3. type指定で型を明示する
Terraformのvariableが対応する主な型は以下のとおりです。型を明示することで、誤った値の注入をplan実行時に検出できます。| 型 | 例 | 用途 |
|---|---|---|
string |
"ap-northeast-1" |
リージョン・環境名・AMI ID |
number |
3 |
インスタンス数・ポート番号 |
bool |
true |
機能ON/OFFのフラグ |
list(string) |
["10.0.1.0/24", "10.0.2.0/24"] |
CIDRブロック・AZのリスト |
map(string) |
{dev="t3.micro", prd="t3.large"} |
環境別の設定マッピング |
object({}) |
{name=string, port=number} |
複数属性をまとめた複合型 |
4. validationブロックでバリデーションを追加する
Terraform 0.13から追加されたvalidationブロックを使うと、変数値の検証ルールをHCL内に書けます。variable "env" { type = string description = "環境名(dev/stg/prd のいずれか)" validation { condition = contains(["dev", "stg", "prd"], var.env) error_message = "env は dev・stg・prd のいずれかを指定してください。" } }
terraform planを実行した時点でバリデーションされ、誤った値が入力されると以下のようにエラーが出ます。$ terraform plan -var='env=production' Error: Invalid value for variable on variables.tf line 1: 1: variable "env" { env は dev・stg・prd のいずれかを指定してください。
ただし、variable validationはplanの冒頭で評価されるため、参照できる値は入力変数の値のみです。「このAMI IDがそのリージョンに実際に存在するか」「IAMロールが指定のS3バケットにアクセスできるか」といった実行時のコンテキストに依存した条件は検証できません。このような実行時検証には、後述のpreconditionを使います。
terraform.tfvarsと*.auto.tfvarsで環境を切り替える
variableにdefaultを設定しなかった場合、値の指定が必須になります。このとき使うのが「.tfvars」ファイルです。1. terraform.tfvarsの基本
Terraformはterraform plan/applyの実行ディレクトリに「terraform.tfvars」または「terraform.tfvars.json」があると自動で読み込みます。# terraform.tfvars(デフォルト・開発環境用) env = "dev" instance_type = "t3.micro" ami_id = "ami-0abcdef1234567890"
2. 環境別のtfvarsファイルで本番・ステージングを切り替える
terraform.tfvarsは自動で読み込まれますが、環境別ファイルは-var-fileオプションで明示指定します。このパターンが環境分離の基本です。# dev.tfvars env = "dev" instance_type = "t3.micro" db_instance = "db.t3.micro" # prd.tfvars env = "prd" instance_type = "t3.large" db_instance = "db.m6g.large"
# 開発環境でplanする $ terraform plan -var-file=dev.tfvars # 本番環境でplanする $ terraform plan -var-file=prd.tfvars
3. *.auto.tfvarsの自動読み込みルール
「.auto.tfvars」で終わるファイルは、実行ディレクトリ内に置くだけで自動的に読み込まれます。複数ファイルを分割して自動読み込みさせたいときに使います。複数の「.auto.tfvars」が存在する場合はファイル名のアルファベット順で読み込まれ、後から読み込まれたファイルの値が優先されます。意図せず値が上書きされないよう、ファイル名と変数の割り当てを整理しておくことが大切です。>> Terraform実践セミナーの詳細はこちら
locals{}で中間値を整理しコードの重複を排除する
locals{}は、variableの値を加工した中間値や、複数箇所で繰り返し使う式をまとめるための機能です。variableと似ていますが、次の2点が異なります。外部から値を渡すことができない(.tfvarsや-varで上書きできない)点と、呼び出し元のモジュールから変更・参照ができない「モジュール内部専用の変数」という位置づけである点です。「モジュール内のプライベート変数」と考えると理解しやすいです。1. localsの基本
# locals.tf locals { # 環境名とプロジェクト名を組み合わせてプレフィックスを生成 prefix = "${var.env}-${var.project}" # 全リソースに付与する共通タグ common_tags = { Environment = var.env Project = var.project ManagedBy = "terraform" } }
local.変数名という形式で参照します(var.と混同しないよう注意してください)。resource "aws_instance" "web" { ami = var.ami_id instance_type = var.instance_type tags = merge(local.common_tags, { Name = "${local.prefix}-web" }) } resource "aws_instance" "app" { ami = var.ami_id instance_type = var.instance_type tags = merge(local.common_tags, { Name = "${local.prefix}-app" }) }
local.common_tagsを1箇所で定義することで、タグの追加・変更が全リソースに一括反映されます。2. mapを使った環境別の値切り替え
localsとvariableを組み合わせて、環境ごとに異なる値を辞書形式で定義するパターンも便利です。locals { # 環境名をキーにしてインスタンスタイプをマッピング instance_type_map = { dev = "t3.micro" stg = "t3.small" prd = "t3.large" } # var.envで現在の環境のインスタンスタイプを選択 selected_instance = local.instance_type_map[var.env] } resource "aws_instance" "web" { ami = var.ami_id instance_type = local.selected_instance # envに応じたタイプが自動選択 }
3. localsを使うべき場面の判断基準
同じ式・計算結果を2箇所以上に書いていたらlocalsで抽出するサインです。次の表を判断基準にしてください。| 状況 | 使うべき要素 |
|---|---|
| 外部から値を渡したい(環境名・リージョン等) | variable{} |
| variableを加工した中間値(prefix・タグマップ等)を使いまわしたい | locals{} |
| モジュール内部でのみ使い、呼び出し元に見せたくない計算値 | locals{}(外部から変更不可) |
| 作成後のリソース属性を外部に公開したい | output{} |
output{}でモジュールの出力値を設計する
output{}は、Terraformが管理するリソースの属性値を外部に公開するための機能です。ルートモジュールでの確認用途のほか、モジュール化した構成では呼び元が子モジュールの出力を参照するために不可欠です。1. outputの基本
# outputs.tf output "vpc_id" { description = "作成したVPCのID" value = aws_vpc.main.id } output "web_instance_public_ip" { description = "Webインスタンスのパブリック IP" value = aws_instance.web.public_ip }
terraform apply後、定義したoutputの値がターミナルに表示されます。後から確認するにはterraform outputコマンドを使います。$ terraform output vpc_id = "vpc-0abcdef1234567890" web_instance_public_ip = "54.238.xxx.xxx" # JSON形式で取得 $ terraform output -json
2. sensitive = trueでシークレット値を保護する
DBのパスワードや秘密鍵など、機密性の高い値をoutputにする場合はsensitive = trueを設定します。output "db_password" { description = "RDSインスタンスのパスワード" value = aws_db_instance.main.password sensitive = true # terraform apply の出力とterraform showでは隠蔽される }
terraform applyの出力上では「(sensitive value)」と表示されます。ログへの漏洩を防ぐためにも、パスワードやAPIキーを含むoutputには必ずsensitive = trueをつける習慣を持ってください。3. モジュールの出力を呼び元から参照する
モジュール化された構成では、子モジュールのoutputを呼び元のルートモジュールから参照できます。outputには「呼び元が実際に必要とする値だけを公開する」ことが重要で、モジュール内部のリソースIDや計算値を無差別に公開すると、呼び元との結合が密になりリファクタリングが困難になります。# modules/vpc/outputs.tf(子モジュール側) output "public_subnet_id" { value = aws_subnet.public.id } # ルートモジュールのmain.tf(呼び元) module "vpc" { source = "./modules/vpc" env = var.env } resource "aws_instance" "web" { ami = data.aws_ami.amazon_linux.id subnet_id = module.vpc.public_subnet_id # module.<名前>.
data{}ソースで既存リソースを参照する
data{}は、Terraformで管理していない既存のリソースをコードから参照するための機能です。「Terraformで作った」ものではなく「すでにAWSに存在する」リソースをコード上で扱いたいときに使います。1. data sourceの基本 -- 既存VPCのID取得
# 既存のVPCをタグで検索して参照する data "aws_vpc" "existing" { filter { name = "tag:Name" values = ["existing-production-vpc"] } } # 取得したVPCのIDを使ってサブネットを作成 resource "aws_subnet" "new" { vpc_id = data.aws_vpc.existing.id cidr_block = "10.0.100.0/24" availability_zone = "ap-northeast-1a" }
data.タイプ.名前.属性の形式です。2. AMI IDをdata sourceで動的に取得する
AMI IDをハードコードすると、AMIが更新されるたびに手動で書き換える必要があります。data sourceを使えば常に最新のAMIを自動選択できます。data "aws_ami" "amazon_linux" { most_recent = true owners = ["amazon"] filter { name = "name" values = ["al2023-ami-*-x86_64"] } } resource "aws_instance" "web" { ami = data.aws_ami.amazon_linux.id # 最新AMIが自動選択される instance_type = var.instance_type }
3. aws_caller_identityでAWSアカウントIDを動的に取得する
マルチアカウント構成でIAMポリシーやリソースARNにアカウントIDを埋め込む場合、アカウントIDをハードコードするとアカウント間でコードが使い回しできなくなります。aws_caller_identityを使えばplan実行時のAWSアカウントIDを動的に取得できます。# 現在のAWSアカウントIDをdata sourceで取得 data "aws_caller_identity" "current" {} locals { account_id = data.aws_caller_identity.current.account_id } # ARN内にアカウントIDをハードコードせず動的に埋め込む resource "aws_iam_policy" "example" { policy = jsonencode({ Version = "2012-10-17" Statement = [{ Effect = "Allow" Action = ["s3:GetObject"] Resource = "arn:aws:s3:::my-bucket-${local.account_id}/*" }] }) }
4. resource{}とdata{}の使い分け
・resource{}:Terraformで作成・変更・削除するリソース(tfstateで追跡される)・data{}:既存リソースを参照するだけ(tfstateでは作成・変更は行われない)
「Terraformに管理させるか、参照だけするか」が判断基準です。既存の共有VPCやIAMロールなど、別チームが管理するリソースはdata sourceで参照するのが原則です。
lifecycle{}のprecondition・postconditionで実行時アサーションを追加する
variable validationは入力変数の形式チェックに優れていますが、planの冒頭でしか評価されず、参照できるのは入力変数の値のみです。「このAMI IDがそのリージョンに存在するか」「作成したセキュリティグループのVPC IDが期待値と一致するか」といった実行時のコンテキストに依存した条件は検証できません。Terraform 1.2で導入されたprecondition・postconditionはこのギャップを埋めます。lifecycleブロック内にアサーションを記述することで、設定ミスをapply段階で即座に検知できます。現場での安全なIaC運用はvariable validationとprecondition/postconditionの多段防御が鉄則です。
1. variable validationとpreconditionの評価タイミングを理解する
4つの仕組みを「いつ」「何を」検証するかで整理します。| 仕組み | 評価タイミング | 参照できる値 | 失敗時の動作 |
|---|---|---|---|
| variable validation | planの冒頭 | 入力変数の値のみ | planを即停止 |
| precondition | リソース評価の直前 | 変数・data sourceの参照値 | applyを即停止 |
| postcondition | リソース作成・更新の直後 | 作成後の属性(self) | applyを停止(ロールバックなし) |
| check block | plan/apply完了後 | data source・リソース属性 | Warningのみ・applyは続行 |
2. preconditionでリソース作成前の条件をチェックする
preconditionはlifecycleブロック内に記述します。conditionがfalseになった瞬間、applyが停止してerror_messageが表示されます。次の例は、data sourceで取得したAMIのアーキテクチャを検証して、インスタンスタイプとの不整合を実行前に検知します。# data sourceでAMIの実体を取得してpreconditionで検証する data "aws_ami" "selected" { most_recent = true owners = ["amazon"] filter { name = "image-id" values = [var.ami_id] } } resource "aws_instance" "web" { ami = data.aws_ami.selected.id instance_type = var.instance_type lifecycle { precondition { # AMIアーキテクチャとインスタンスタイプの整合性を実行前に検証する condition = ( can(data.aws_ami.selected.architecture == "x86_64") && startswith(var.instance_type, "t3.") ) error_message = "t3系インスタンスにはx86_64 AMIが必要です。AMIのアーキテクチャ: ${data.aws_ami.selected.architecture}" } } }
$ terraform apply Planning failed. Terraform encountered an error while generating this plan. | Error: Resource precondition failed | | on main.tf line 24, in resource "aws_instance" "web": | 24: condition = ( | | t3系インスタンスにはx86_64 AMIが必要です。AMIのアーキテクチャ: arm64
3. postconditionでリソース作成後の状態を検証する
postconditionはリソースの作成・更新が完了した直後に評価されます。postcondition内ではselfを使って、今まさに作成・更新されたリソース自身の属性を参照できます。これはpreconditionでは使えないpostcondition固有の機能です。resource "aws_security_group" "web" { name = "web-sg" vpc_id = var.vpc_id ingress { from_port = 443 to_port = 443 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } lifecycle { postcondition { # selfで作成されたリソース属性を参照して検証する condition = self.vpc_id == var.vpc_id error_message = "セキュリティグループのVPC IDが期待値と異なります。expected: ${var.vpc_id}, actual: ${self.vpc_id}" } } }
4. check blockで継続的な状態監視をする(Terraform 1.5+)
Terraform 1.5で導入されたcheck blockは、特定のリソースに紐付くのではなくインフラ全体の状態をterraform planの実行ごとに継続的に検証します。条件がfalseになってもWarningとして報告されるだけでapplyは続行します。「壊れたら止める」ではなく「定期的に状態を監視する」ための仕組みです。# Terraform 1.5以降: checkブロックでALBエンドポイントを継続監視する check "https_health_check" { data "http" "lb_endpoint" { url = "https://${aws_lb.main.dns_name}/health" } assert { condition = data.http.lb_endpoint.status_code == 200 error_message = "ロードバランサーのヘルスチェックが200を返していません。status: ${data.http.lb_endpoint.status_code}" } }
変数ファイルの推奨構成パターン
実務で崩れにくいファイル構成を紹介します。project/ ├── main.tf # resource{}ブロックを中心に記述 ├── variables.tf # variable{}をすべてここに集約 ├── locals.tf # locals{}を集約(mainに書いてもよいが分離を推奨) ├── outputs.tf # output{}をすべてここに集約 ├── provider.tf # provider{}とbackend{}の設定 ├── versions.tf # required_version・required_providersのバージョン制約 ├── terraform.tfvars # デフォルト(dev)の変数値(自動読み込み) ├── stg.tfvars # ステージング環境用(-var-file指定が必要) └── prd.tfvars # 本番環境用(-var-file指定が必要)
versions.tfは省略されがちですが、チーム開発では重要です。TerraformのバージョンとプロバイダーのバージョンをGit管理することで、「メンバーごとにTerraformのバージョンが違う」「プロバイダーが勝手にアップデートされた」という問題を防止できます。# versions.tf terraform { required_version = ">= 1.5.0" required_providers { aws = { source = "hashicorp/aws" version = ">= 5.0.0" } } }
-var-file=prd.tfvarsのように明示指定が必要です。「自動読み込みされると思っていたら本番用の値が使われなかった」というミスは頻発します。また、prd.tfvarsにはパスワードやシークレットキーを書かず、これらはTF_VAR_変数名という環境変数かAWS Secrets Managerを使って注入する運用が推奨されます。Gitリポジトリにシークレットをコミットしないようにするためにもprd.tfvarsは.gitignoreへの追加を検討してください。
よくあるエラーと対処法
1. "No value for required variable"
Error: No value for required variable on variables.tf line 5: 5: variable "env" { The root module input variable "env" is not set, and has no default value. Use a -var or -var-file command line argument to provide a value for this variable.
対処:-var='env=dev' オプションか .tfvars ファイルで値を指定する。CI/CD環境では
TF_VAR_env=devという環境変数でも設定できる。2. "Variables may not be used here"
Error: Variables not allowed on variables.tf line 6, in variable "env": 6: default = var.aws_region Variables may not be used here.
対処:variableのdefaultにはリテラル値のみ指定できる。可変的なデフォルトが必要な場合はlocals{}で計算する。
3. data sourceのフィルタが一致しない
Error: Your query returned no results. Please change your search criteria and try again. with data.aws_vpc.existing, on main.tf line 3, in data "aws_vpc" "existing":
対処:AWSコンソールまたはAWS CLIで対象リソースの存在とタグ名を確認してから再実行する。
# タグで既存VPCを確認する(AWS CLI) $ aws ec2 describe-vpcs --filters "Name=tag:Name,Values=existing-production-vpc" --query 'Vpcs[*].{ID:VpcId,Name:Tags[?Key==`Name`]|[0].Value}'
4. sensitive変数がCI/CDログに出力されてしまう
原因:outputにsensitive = trueを設定していない、またはscript内でterraform outputを実行したログが保存されている対処:機密値を含むoutputには必ずsensitive = trueを追加する。CI/CDでoutputを取得する場合はterraform output -rawやjqを使って特定の値のみ取り出し、ログマスクの設定も合わせて行う。
5. preconditionのconditionで評価エラーが発生する
precondition/postconditionのconditionに参照エラーが起きる式を書くと、error_messageより先にTerraformの実行エラーになります。can()関数でラップすると、評価エラー自体をfalseとして扱えるため、意図したerror_messageが表示されるようになります。# NG: data.aws_ami.selectedが取得できない場合に評価エラーが発生する condition = data.aws_ami.selected.architecture == "x86_64" # OK: can()でラップすることで評価エラーをfalseに変換する condition = can(data.aws_ami.selected.architecture == "x86_64")
6. モジュールのoutputを呼び元で参照できない
Error: Unsupported attribute on main.tf line 20, in resource "aws_instance" "web": 20: subnet_id = module.vpc.aws_subnet.public.id This object does not have an attribute named "aws_subnet".
module.vpc.aws_subnet.public.idのように直接参照しようとした。モジュール内部のリソースは外部から直接参照できない。対処:子モジュール側のoutputs.tfにoutputブロックを追加して値を公開し、呼び元では
module.vpc.public_subnet_idのようにmodule.名前.output名の形式で参照する。本記事のまとめ
HCL変数設計の要素をまとめます。
| 要素 | 用途 | 参照・記述方法 |
|---|---|---|
| variable{} | 外部から注入する入力値。型・デフォルト・バリデーションを定義する | var.変数名 |
| locals{} | 内部で使う中間値・共通タグなど。DRY化とコード整理に使う(外部変更不可) | local.変数名 |
| output{} | モジュールから外部へ公開する値。sensitive指定で機密値を保護する | module.名前.output名 |
| data{} | 既存リソースをコードから参照する(作成・変更は行わない) | data.タイプ.名前.属性 |
| precondition | リソース作成前に実行時コンテキストを検証する。data sourceの結果も参照可能 | lifecycle { precondition } |
| postcondition | リソース作成後の属性(self)を検証する。ARN・VPC IDの整合性確認に使う | lifecycle { postcondition } |
設計の基本原則は4つです。
・ハードコードを見つけたらvariableに置き換え、値はtfvarsで注入する・同じ式や加工ロジックを複数箇所に書いたらlocalsに抽出する
・モジュール化したらoutputで外部インターフェースを定義し、呼び元はmodule.名前.output名で参照する
・Terraformで管理していないリソースの参照はdata sourceで行い、resourceに混在させない
さらに、variable validationとprecondition/postconditionを組み合わせた多段防御が、現場での安全なIaC運用の鉄則です。入力値の形式はvalidationで弾き、実行時コンテキストはpreconditionで確認し、作成後の属性はpostconditionで検証する。この3段構えを習慣化することで、「applyが通ったのに想定と違う状態になっていた」という事後対応型のトラブルを大幅に減らせます。
>> Terraform実践セミナーの詳細はこちら
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 次のページへ:Terraformのlifecycleブロックで本番リソースを誤削除から守る方法|prevent_destroyとignore_changesの実践設計
- 前のページへ:TerraformとAnsibleの違いと使い分け|IaCツール選定の判断軸
- この記事の属するカテゴリ:Terraformへ戻る

無料メルマガで学習を続ける
Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。
登録無料・いつでも解除できます