TerraformのHCL型制約とvalidationブロックでモジュール入力を安全に設計する方法|object・list・mapのtype constraintと実践パターン

宮崎智広 この記事の監修:宮崎智広(Linux実務・教育歴20年以上・受講者3,100名超)
HOME > Linux技術 リナックスマスター.JP(Linuxマスター.JP) > Terraform > TerraformのHCL型制約とvalidationブロックでモジュール入力を安全に設計する方法|object・list・mapのtype constraintと実践パターン
「このモジュール、型を何も指定していなかったせいで、文字列を渡すべき変数にリストを渡してしまった」「環境名に想定外の値を渡してapplyが通ってしまった」——TerraformのModuleを複数人で使い始めると、型や値の誤りをapplyするまで気づけないケースが増えてきます。

HCL(HashiCorp Configuration Language)には、変数に型制約(type constraint)を付ける仕組みと、値の正当性を検査するvalidationブロックが用意されています。型制約を正しく使えばterraform validateの段階で型の誤りを検出でき、validationブロックを組み合わせることで「環境名はdev・stg・prodの3値のみ」「CIDRは/16以上/28以下にする」といったビジネスルールをコードで表現できます。

この記事では、Terraform 1.3以降の環境(RHEL 9.4 / Ubuntu 24.04 LTSで動作確認済み)を前提に、HCL型システムの全体像、objectによる構造型の活用、validationブロックのカスタマイズ方法を実践パターンとともに解説します。

この記事のポイント

・型制約なし変数はterraform validateで型ミスを検出できない
・object({})でモジュール入力を厳密に定義し、apply前にエラーを止める
・validationブロックで値の範囲・パターンをコードに落とし込める
・optional()(Terraform 1.3以降)でデフォルト付き省略可能フィールドを作れる


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

型制約なし変数が引き起こす問題

Terraformのvariableブロックは、typeを省略しても動作します。しかし省略したままModuleを複数人で共有すると、次のような問題が起きます。

1. 誤った型が渡されてもvalidateで検出できない
typeを指定しない変数はanyと同等に扱われます。文字列を期待する変数にリストを渡しても、terraform validateは通過し、実際にterraform applyでリソースを変更しようとした段階で初めてエラーになります。

2. 入力インターフェースが読み取れない
型定義がないと、変数に何を渡せばよいかをコードから読み解けません。コメントやREADMEに頼ることになり、ドキュメントとコードが乖離するリスクが生まれます。

3. 誤った値が素通りして本番に影響する
「dev」「stg」「prod」以外の環境名を想定していても、型制約だけでは文字列として通ってしまいます。validationを使わないと、意図しない値でインフラが作られる可能性があります。

HCL型システムの全体像

TerraformのHCL型は大きく3カテゴリに分かれます。

カテゴリ 型名 概要
プリミティブ型 string, number, bool 最も基本的な値の型
コレクション型 list(型), set(型), map(型) 同じ型の値を複数保持する
構造型 object({属性=型, ...}), tuple([型, ...]) 属性ごとに異なる型を持てる
anyはどの型にも一致する特殊な型です。型制約を書かない変数はanyと同等になります。

map(string)とobject({})の違い
よく混同されやすいのがmap(string)とobject({})の違いです。

・map(string): キーは任意だが、全値がstring型でなければならない
・object({name=string, port=number}): キーが固定で、属性ごとに異なる型を指定できる

設定項目が固定されている場合はobjectを使うのが適切です。

プリミティブ型とコレクション型の書き方

まず基本となるプリミティブ型とコレクション型の使い方を確認します。

1. プリミティブ型(string・number・bool)

# variables.tf variable "environment" { type = string description = "デプロイ環境名(dev/stg/prod)" } variable "instance_count" { type = number description = "EC2インスタンス数" default = 1 } variable "enable_https" { type = bool description = "HTTPSを有効化するか" default = true }

2. コレクション型(list・set・map)

variable "allowed_cidrs" { type = list(string) description = "許可するCIDRのリスト" default = ["10.0.0.0/16", "172.16.0.0/12"] } variable "subnet_ids" { type = set(string) description = "使用するサブネットIDの集合(重複なし)" } variable "tags" { type = map(string) description = "リソースに付与するタグ" default = {} }

listとsetの違いは、setが重複を許可しない点です。サブネットIDのように一意性が重要な場合はsetを使います。

構造型(object)でモジュール入力を厳密に定義する

複数の設定項目をまとめてModuleに渡す場合、objectを使うと入力の構造をコードで表現できます。

1. objectの基本的な書き方

# RDSインスタンスの設定をひとまとめにする例 variable "rds_config" { type = object({ instance_class = string allocated_storage = number multi_az = bool engine_version = string }) description = "RDSインスタンスの設定" } # 呼び出し側(terraform.tfvarsまたはmodule呼び出し) rds_config = { instance_class = "db.t3.medium" allocated_storage = 100 multi_az = true engine_version = "8.0" }

objectの全属性を渡す必要があるため、渡し忘れがあるとvalidate時点でエラーになります。

2. ネストしたobjectでサブネット設定をまとめる

variable "network_config" { type = object({ vpc_cidr = string subnets = list(object({ cidr = string az = string })) }) } # 呼び出し例 network_config = { vpc_cidr = "10.0.0.0/16" subnets = [ { cidr = "10.0.1.0/24", az = "ap-northeast-1a" }, { cidr = "10.0.2.0/24", az = "ap-northeast-1c" }, ] }

list(object({...}))のネスト構造を使うと、複数サブネットの定義を型付きリストとして受け取れます。

validationブロックで値の制約とエラーメッセージをカスタマイズする

型制約はデータ型の正しさを保証しますが、「環境名はdev・stg・prodだけ」「ポート番号は1024以上65535以下」といったビジネスルールは型だけでは表現できません。これを補うのがvalidationブロックです。

1. enumパターン(許可値のリストを指定)

variable "environment" { type = string validation { condition = contains(["dev", "stg", "prod"], var.environment) error_message = "environment は dev / stg / prod のいずれかを指定してください。" } }

2. 数値範囲チェック

variable "app_port" { type = number validation { condition = var.app_port >= 1024 && var.app_port <= 65535 error_message = "app_port は 1024 から 65535 の間で指定してください。" } }

3. 正規表現パターンマッチ(CIDRの形式チェック)

variable "vpc_cidr" { type = string validation { condition = can(cidrhost(var.vpc_cidr, 0)) error_message = "vpc_cidr は有効なCIDR表記(例: 10.0.0.0/16)で指定してください。" } }

can()関数を使うと、cidrhost()が成功するかどうかでCIDRの妥当性を検査できます。

4. validation失敗時の出力例

不正な値でterraform planを実行した場合、次のようなエラーが表示されます。

# 不正な環境名を渡した場合 $ terraform plan -var="environment=test" │ Error: Invalid value for variable │ │ on variables.tf line 1: │ 1: variable "environment" { │ ├──────────────── │ │ var.environment is "test" │ │ environment は dev / stg / prod のいずれかを指定してください。 │ │ This was checked by the validation rule at variables.tf:4,3-13. ╵

terraform plan(インフラ変更なし)の段階で明確なエラーメッセージが出るため、原因特定が容易になります。

optional()で省略可能なフィールドを扱う(Terraform 1.3以降)

objectを使う場合、全属性の指定が必須になります。しかし設定項目によっては省略したいケースもあります。Terraform 1.3からoptional()モディファイアを使って省略可能な属性を定義できます。

1. optional()の基本構文

variable "db_config" { type = object({ instance_class = string # 必須 allocated_storage = number # 必須 multi_az = optional(bool, false) # 省略時は false backup_retention = optional(number, 7) # 省略時は 7(日) parameter_group = optional(string) # 省略時は null }) } # 最小構成で渡す例(必須項目のみ) db_config = { instance_class = "db.t3.small" allocated_storage = 20 }

optional(型, デフォルト値)の形式でデフォルト値を設定できます。デフォルト値を省略した場合はnullが入ります。

2. Terraform 1.3未満との互換性

Terraform 1.2以前のプロジェクトでoptional()を使うと、初期化時にエラーが出ます。required_providersでバージョンを固定し、チーム全員が1.3以降を使っていることを確認してから導入しましょう。Terraformのセミナーや実務では、バージョン管理の統一が先決です。

型制約とvalidationを組み合わせた実践的なモジュール設計

ここまでの内容を組み合わせた、実際のModuleのvariables.tf例を示します。チームでTerraformを運用する際のTerraformセミナーでも取り上げられる設計パターンです。

ALBリスナーモジュールの入力定義例

# modules/alb_listener/variables.tf variable "environment" { type = string description = "デプロイ環境(dev/stg/prod)" validation { condition = contains(["dev", "stg", "prod"], var.environment) error_message = "environment は dev / stg / prod を指定してください。" } } variable "listener_config" { type = object({ protocol = string port = number ssl_policy = optional(string, "ELBSecurityPolicy-TLS13-1-2-2021-06") certificate_arn = optional(string) }) description = "ALBリスナーの設定" validation { condition = contains(["HTTP", "HTTPS"], var.listener_config.protocol) error_message = "protocol は HTTP または HTTPS を指定してください。" } validation { condition = var.listener_config.protocol != "HTTPS" || var.listener_config.certificate_arn != null error_message = "protocol が HTTPS の場合は certificate_arn を指定してください。" } } variable "target_group_arns" { type = list(string) description = "転送先ターゲットグループARNのリスト" validation { condition = length(var.target_group_arns) >= 1 error_message = "target_group_arns には最低1つのARNを指定してください。" } }

このように複数のvalidationブロックを1つのvariableに置くことができます。「HTTPSなのにcertificate_arnがnull」という構成ミスをplan前に止められます。

よくあるエラーと原因の切り分け

1. 「The given value is not suitable for var.xxx」が出る場合

型が一致していません。渡した値の型とvariableのtype制約が食い違っています。

・よくある原因: tfvarsでstring型のつもりでnumberを書いた(または逆)
・確認方法: terraform consoleを使い、var.xxxで実際に渡っている値を確認する

2. 「Value must be known」が出る場合

objectの属性にknown after applyの値を渡しています。plan段階でまだ決まっていない値をvalidationのconditionに使うと失敗します。validationで参照する値はplan時に確定するものに限定しましょう。

3. 「The argument "type" is required」が出る場合

Terraform 0.11以前の旧構文(type = "string"のようにダブルクォートで囲む)が混在しています。現在のTerraformでは型名はクォートなしの識別子として書きます(type = string)。

4. optional()で「The "optional" function may not be used in this context」が出る場合

Terraform 1.3未満を使っています。terraform versionでバージョンを確認し、1.3以降にアップグレードするか、optional()の使用を諦めてdevault値なしのobjectに変更してください。

本記事のまとめ

やりたいこと 書き方
文字列型の変数を定義する type = string
文字列リストの変数を定義する type = list(string)
複数属性をまとめて受け取る type = object({name=string, port=number})
省略可能な属性をデフォルト付きで定義する optional(bool, false)(Terraform 1.3以降)
許可値をリストで制限する contains(["dev","stg","prod"], var.env)
数値範囲を制限する var.port >= 1024 && var.port <= 65535
CIDRの書式を検証する can(cidrhost(var.cidr, 0))
HTTPS時にcertificate必須にする validationブロックで条件式を組み合わせる
TerraformのHCL型制約とvalidationブロックを活用すると、型の誤りや値の不正をterraform plan(インフラ変更なし)の段階で止められます。特にチームでModuleを共有する場合、objectで入力構造を定義し、validationで制約をコード化しておくことで、レビュー負荷の軽減と設定ミスの早期発見につながります。
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、TerraformをはじめとするIaCツールの実践スキルを学べるTerraform実践セミナーをご覧ください。20年以上の現場経験を持つ講師が、型設計・モジュール設計・CI/CD連携まで丁寧に解説します。

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

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

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

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

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

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

姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

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

この記事を書いた人

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

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

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