TerraformのHCL変数設計|variable・locals・output・data sourceで構成を整理する方法

宮崎智広 この記事の監修:宮崎智広(Linux実務・教育歴20年以上・受講者3,100名超)
HOMELinux技術 リナックスマスター.JP(Linuxマスター.JP)Terraform > TerraformのHCL変数設計|variable・locals・output・data sourceで構成を整理する方法
「TerraformでインフラをHCLに落としたはいいが、同じリージョン名や環境名があちこちにハードコードされていて管理できない」
「開発・本番の切り替えでファイルを書き換えたら、うっかり本番用の値で開発環境を壊してしまった」

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で作っていない既存リソース」をコードから参照するための機能


「このままじゃマズい」と感じていませんか?
参考書を開く気力もない、同年代に取り残される不安——
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
図解60P/登録10秒/解除も3秒 / 詳細はこちら

なぜ変数設計が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" } }

このコードの問題点は3つあります。AMI IDとサブネットIDが複数箇所に書かれており、変更時に修正漏れが起きやすいこと。「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" }

定義したvariableは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 のいずれかを指定してください。

本番環境への誤った設定値流入を防ぐうえで、特に環境名やインスタンスタイプのvalidationは有効です。

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

ファイルを切り替えるだけで、インスタンスタイプやDB容量が環境に合った値になります。本番用ファイルを誤ってコピー・変更するリスクが排除できます。

3. *.auto.tfvarsの自動読み込みルール

「.auto.tfvars」で終わるファイルは、実行ディレクトリ内に置くだけで自動的に読み込まれます。複数ファイルを分割して自動読み込みさせたいときに使います。ただし、複数の「.auto.tfvars」が存在すると読み込み順の予測が難しくなるため、使いすぎには注意が必要です。
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、TerraformのHCL変数設計から環境分離・module設計まで、ハンズオン形式で体系的に学べるセミナーを開催しています。
>> Terraform実践セミナーの詳細はこちら

locals{}で中間値を整理しコードの重複を排除する

locals{}は、variableの値を加工した中間値や、複数箇所で繰り返し使う式をまとめるための機能です。「モジュール内のプライベート変数」に相当すると考えると理解しやすいです。

1. localsの基本

# locals.tf locals { # 環境名とプロジェクト名を組み合わせてプレフィックスを生成 prefix = "${var.env}-${var.project}" # 全リソースに付与する共通タグ common_tags = { Environment = var.env Project = var.project ManagedBy = "terraform" } }

定義したlocalsは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では隠蔽される }

sensitiveを設定すると、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指定が必要)

重要:terraform.tfvarsは自動で読み込まれますが、stg.tfvarsやprd.tfvarsは必ず-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.

原因:defaultのない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{}の定義内でvar.を使おうとした(循環参照)
対処: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":

原因:data sourceのfilter条件に一致するリソースがAWS上に存在しない
対処: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統合もスムーズになります。
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、Terraformの変数設計からCI/CDパイプライン統合・モジュール設計まで、実務で使えるIaCスキルをハンズオン形式で習得できるセミナーを開催しています。
>> Terraform実践セミナーの詳細はこちら

無料メルマガで学習を続ける

Linuxの実践スキルをメールで毎週お届け。
登録は1分、解除もいつでも可。

登録無料・いつでも解除できます

暗記不要・1時間後にはサーバーが動く

3,100名以上が実践した「型」を無料で公開中

プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。

登録10秒/合わなければ解除3秒 / 詳細はこちら

Linux無料マニュアル(図解60P) 名前とメールで30秒登録
宮崎 智広

この記事を書いた人

宮崎 智広(みやざき ともひろ)

株式会社イーネットマーキュリー代表。現役のLinuxサーバー管理者として20年以上の実務経験を持ち、これまでに累計3,100名以上のエンジニアを指導してきたLinux教育のプロフェッショナル。「現場で本当に使える技術」を体系的に伝えることをモットーに、実践型のLinuxセミナーの開催や無料マニュアルの配布を通じてLinux人材の育成に取り組んでいる。

趣味は、キャンプにカメラ、トラウト釣り。好きな食べ物は、ラーメンにお酒。休肝日が作れない、酒量を減らせないのが悩み。最近、ドラマ「フライトエンジェル」を観て涙腺が崩壊しました。