「開発・本番の切り替えでファイルを書き換えたら、うっかり本番用の値で開発環境を壊してしまった」
Terraformを触り始めたころ、こういった失敗は誰もが経験します。その根本原因は、Terraformの変数機能を正しく使っていないことにあります。
TerraformのHCLには「variable」「locals」「output」「data source」という4種類の変数・参照機能があります。これらを使い分けることで、コードの重複を排除し、環境ごとの値切り替えをファイルコピーなしに実現できます。
この記事では、HCL変数設計の4要素を実際のコードと実行例で解説します。variables.tfの型定義・バリデーション・tfvarsによる環境分離・locals/outputの使い所・data sourceによる既存リソース参照まで、設計軸で整理して体得できます。
実行環境:Terraform 1.8.x(RHEL 9.4 / Ubuntu 24.04 LTS で動作確認済み)
この記事のポイント
・variable{}で入力値を外部化し、terraform.tfvarsで環境ごとに安全に切り替えられる
・locals{}は繰り返し使う式や計算結果を1箇所にまとめてDRYにするための要素
・output{}はモジュールから外部へ値を公開し、モジュール間の疎結合を保つ
・data{}ソースは「Terraformで作っていない既存リソース」をコードから参照するための機能
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
なぜ変数設計が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要素を使いこなすことで、これらを解消できます。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. 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} |
複数属性をまとめた複合型 |
3. 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 のいずれかを指定してください。
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の値を加工した中間値や、複数箇所で繰り返し使う式をまとめるための機能です。「モジュール内のプライベート変数」に相当すると考えると理解しやすいです。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に応じたタイプが自動選択 }
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を呼び元のルートモジュールから参照できます。# 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. resource{}とdata{}の使い分け
・resource{}:Terraformで作成・変更・削除するリソース(tfstateで追跡される)・data{}:既存リソースを参照するだけ(tfstateでは作成・変更は行われない)
「Terraformに管理させるか、参照だけするか」が判断基準です。既存の共有VPCやIAMロールなど、別チームが管理するリソースはdata sourceで参照するのが原則です。
変数ファイルの推奨構成パターン
実務で崩れにくいファイル構成を紹介します。project/ ├── main.tf # resource{}ブロックを中心に記述 ├── variables.tf # variable{}をすべてここに集約 ├── locals.tf # locals{}を集約(mainに書いてもよいが分離を推奨) ├── outputs.tf # output{}をすべてここに集約 ├── provider.tf # provider{}とbackend{}の設定 ├── terraform.tfvars # デフォルト(dev)の変数値(自動読み込み) ├── stg.tfvars # ステージング環境用(-var-file指定が必要) └── prd.tfvars # 本番環境用(-var-file指定が必要)
-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を使って特定の値のみ取り出し、ログマスクの設定も合わせて行う。
本記事のまとめ
HCL変数設計の4要素をまとめます。
| 要素 | 用途 | 参照方法 |
|---|---|---|
| variable{} | 外部から注入する入力値。型・デフォルト・バリデーションを定義する | var.変数名 |
| locals{} | 内部で使う中間値・共通タグなど。DRY化とコード整理に使う | local.変数名 |
| output{} | モジュールから外部へ公開する値。sensitive指定で機密値を保護する | module.名前.output名 |
| data{} | 既存リソースをコードから参照する(作成・変更は行わない) | data.タイプ.名前.属性 |
設計の基本原則は3つです。
・ハードコードを見つけたらvariableに置き換え、値はtfvarsで注入する・同じ式や加工ロジックを複数箇所に書いたらlocalsに抽出する
・モジュール化したらoutputで外部インターフェースを定義し、呼び元はmodule.名前.output名で参照する
この3原則を守るだけで、環境差異によるミスが大幅に減り、複数人でのレビュー・CI/CD統合もスムーズになります。
>> Terraform実践セミナーの詳細はこちら
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
登録10秒/合わなければ解除3秒 / 詳細はこちら
- 次のページへ:Terraformのlifecycleブロックで本番リソースを誤削除から守る方法|prevent_destroyとignore_changesの実践設計
- 前のページへ:TerraformとAnsibleの違いと使い分け|IaCツール選定の判断軸
- この記事の属するカテゴリ:Terraformへ戻る

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