目次
HCL 基礎
これまで3回にわたって Terraform コードを書いてきました。でも、自分が何を書いているか本当に理解していますか?
resource と data の違いや、list(string) と map(string) の使い分けを知らずに、ただ例をコピーしているなら、この Part で全て解決します。今回は HashiCorp Configuration Language (HCL) — Terraform を動かす言語について深く掘り下げていきます。
📦 Code Examples
Repository: terraform-hcl-tutorial-series 今回の Part: Part 4 - HCL Syntax Examples
動作する例を取得:
git clone https://github.com/khuongdo/terraform-hcl-tutorial-series.git
cd terraform-hcl-tutorial-series
git checkout part-04
cd examples/part-04-hcl-fundamentals/
# 例を試してみる
terraform init
terraform plan
HCL が存在する理由
HCL は JSON ではありません。YAML でもありません。HashiCorp はインフラコード専用に開発しました。この選択は、思っている以上に重要です。
JSON は冗長で、コメントをサポートせず、人間の読者を苦しめます。YAML は見た目はきれいですが、インデント地獄に陥り、目に見えないスペース1つでパイプラインが失敗することになります。HCL はより良いものを提供します:コードのように読めるが、インフラを定義する言語です。
得られるもの:
- ファイルに実際に残るコメント
- 静的文字列の代わりに式と関数
terraform applyが週末を台無しにする前にエラーをキャッチする強い型付け- 動的な設定を可能にする補間
- 純粋な JSON では不可能なパターンのための条件分岐とループ
重要なポイント: HCL を理解すると、Terraform の書き方が変わります。Stack Overflow からコピー&ペーストするのをやめて、インフラを設計し始めます。
HCL の仕組み
すべての Terraform ファイルは3つの要素から構成されています:blocks、arguments、expressions。これだけです。
Blocks
Block はコンテナです。型があり、ラベルがある場合もあり、本体は中括弧で囲まれます:
<BLOCK_TYPE> "<LABEL>" "<LABEL>" {
# Arguments がここに入る
<IDENTIFIER> = <EXPRESSION>
}
実際の例:
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
}
variable "region" {
type = string
default = "us-west-2"
}
output "instance_ip" {
value = aws_instance.web.public_ip
}
頻繁に使用する block types:
resourceインフラを作成data既存のものを読み取るvariable入力を宣言output値を公開locals計算された値を定義module再利用可能なモジュールを呼び出すterraformTerraform 自体を設定providerクラウドプロバイダーの認証情報を設定
Arguments
Arguments は値を割り当てます。左側が名前、右側が式です:
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0" # String
instance_type = var.instance_type # Variable reference
count = 3 # Number
}
規約: 1行に1つの argument。diff が読みやすくなります。
Expressions
ここで HCL が便利になります。Expressions は値を計算します:
# リテラル
"hello"
42
true
# 参照
var.instance_type
aws_vpc.main.id
# 算術演算
var.instance_count * 2
# 文字列補間
"Instance: ${var.name}"
# 条件分岐
var.env == "prod" ? "t3.large" : "t3.micro"
# 関数
file("${path.module}/userdata.sh")
jsonencode({ name = "app", port = 8080 })
# ループ
[for s in var.subnets : s.id]
すべてをハードコードする代わりに、コンテキストに基づいて値を計算します。これがパワームーブです。
型システム
HCL には強い型があります。これは良いことです。深夜3時のデプロイ時ではなく、早い段階でエラーをキャッチできるからです。
プリミティブ型
String — テキスト値
variable "region" {
type = string
default = "us-west-2"
}
# 複数行文字列は heredoc を使用
variable "user_data" {
type = string
default = <<-EOF
#!/bin/bash
echo "Hello, World!"
EOF
}
Number — 整数と浮動小数点数 (HCL は区別しません)
variable "instance_count" {
type = number
default = 3
}
variable "disk_size" {
type = number
default = 100.5
}
Bool — true または false
variable "enable_monitoring" {
type = bool
default = true
}
# 一般的なパターン: 条件付きリソース
resource "aws_cloudwatch_alarm" "cpu" {
count = var.enable_monitoring ? 1 : 0
# ...
}
複合型
List — 順序付きコレクション、すべての要素が同じ型
variable "availability_zones" {
type = list(string)
default = ["us-west-2a", "us-west-2b", "us-west-2c"]
}
# インデックスでアクセス
locals {
first_az = var.availability_zones[0]
}
# count でループ
resource "aws_subnet" "public" {
count = length(var.availability_zones)
availability_zone = var.availability_zones[count.index]
cidr_block = "10.0.${count.index}.0/24"
}
Map — key-value ペア (キーは常に文字列)
variable "tags" {
type = map(string)
default = {
Environment = "production"
Owner = "platform-team"
CostCenter = "engineering"
}
}
# キーでアクセス
locals {
env = var.tags["Environment"]
}
# マップをマージ
locals {
all_tags = merge(
var.tags,
{ ManagedBy = "terraform" }
)
}
Set — 順序なし、一意の値
variable "allowed_ports" {
type = set(number)
default = [80, 443, 8080]
}
# for_each で使用
resource "aws_security_group_rule" "ingress" {
for_each = var.allowed_ports
type = "ingress"
from_port = each.value
to_port = each.value
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
Object — 型付き属性を持つ構造化データ
variable "server_config" {
type = object({
instance_type = string
ami_id = string
root_volume = object({
size = number
type = string
})
tags = map(string)
})
default = {
instance_type = "t3.micro"
ami_id = "ami-0c55b159cbfafe1f0"
root_volume = {
size = 20
type = "gp3"
}
tags = {
Name = "web-server"
}
}
}
# ネストされた属性にアクセス
locals {
volume_size = var.server_config.root_volume.size
}
Tuple — 固定長リスト、各位置に特定の型
variable "connection_info" {
type = tuple([string, number, bool])
# 正確に: [string, number, bool]
default = ["db.example.com", 5432, true]
}
locals {
host = var.connection_info[0]
port = var.connection_info[1]
use_ssl = var.connection_info[2]
}
Any — 何でも受け入れる (控えめに使用)
variable "custom_config" {
type = any
default = {}
}
本当に必要な場合を除いて any を避けてください。型チェックをバイパスし、デバッグが困難になります。構造化データがある場合は、代わりに object({...}) を使用してください。
文字列補間とディレクティブ
${} で文字列内に式を埋め込みます:
locals {
region = "us-west-2"
environment = "production"
bucket_name = "${var.project}-${local.environment}-logs"
# 結果: "myapp-production-logs"
}
文字列ディレクティブ (%{}) で文字列内に制御フローを使用:
locals {
user_data = <<-EOF
#!/bin/bash
%{ for ip in var.server_ips ~}
echo "Server: ${ip}"
%{ endfor ~}
EOF
}
文字列内の条件分岐:
locals {
greeting = <<-EOT
Hello, ${var.username}!
%{ if var.is_admin }
You have admin privileges.
%{ else }
You have standard access.
%{ endif }
EOT
}
演算子
算術演算と論理演算はほとんどの言語と同様に機能します:
# 算術演算
var.count + 5
var.price * 1.2
var.total / var.quantity
# 比較
var.count > 10
var.env == "prod"
var.enabled != false
# 論理演算
var.is_prod && var.enable_monitoring
var.is_dev || var.is_staging
!var.disabled
組み込み関数
Terraform は100以上の関数を提供しています。カスタム関数は書けません — 提供されているものを使います。実際に使用するものは以下です。
ファイル関数
file() ファイルを文字列として読み取ります:
locals {
user_data = file("${path.module}/scripts/init.sh")
}
filebase64() ファイルを base64 として読み取ります:
resource "aws_s3_object" "config" {
bucket = aws_s3_bucket.app.id
key = "config.json"
content = filebase64("${path.module}/config.json")
}
templatefile() 変数を使ってテンプレートをレンダリング:
# templates/userdata.tpl
#!/bin/bash
echo "Environment: ${environment}"
echo "Region: ${region}"
# main.tf
locals {
user_data = templatefile("${path.module}/templates/userdata.tpl", {
environment = var.environment
region = var.region
})
}
パス参照
path.module # 現在のモジュールディレクトリ
path.root # ルートモジュールディレクトリ
path.cwd # 現在の作業ディレクトリ
エンコーディング関数
jsonencode() HCL 値を JSON に変換:
locals {
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "s3:GetObject"
Resource = "arn:aws:s3:::my-bucket/*"
}]
})
}
jsondecode() JSON 文字列を解析:
locals {
config_data = jsondecode(file("config.json"))
api_url = local.config_data.api.url
}
yamlencode() と yamldecode() は YAML に対して同じことをします:
locals {
deployment = yamldecode(file("k8s/deployment.yaml"))
replicas = local.deployment.spec.replicas
}
コレクション関数
length() 要素を数えます:
length(["a", "b", "c"]) # 3
length({ key = "value" }) # 1
concat() リストをマージ:
concat(["a", "b"], ["c", "d"]) # ["a", "b", "c", "d"]
merge() マップを結合:
merge(
{ name = "app" },
{ env = "prod" }
)
# 結果: { name = "app", env = "prod" }
lookup() フォールバック付きでマップ値を取得:
lookup(var.tags, "Environment", "unknown")
# 存在すれば var.tags["Environment"] を返し、なければ "unknown" を返す
keys() と values() マップから抽出:
keys({ a = 1, b = 2 }) # ["a", "b"]
values({ a = 1, b = 2 }) # [1, 2]
element() インデックスで要素を取得 (ラップアラウンド):
element(["a", "b", "c"], 0) # "a"
element(["a", "b", "c"], 5) # "c" (5 % 3 = 2)
contains() リストに値が含まれているかチェック:
contains(["prod", "staging"], var.env) # true/false
distinct() 重複を削除:
distinct(["a", "b", "a", "c"]) # ["a", "b", "c"]
flatten() ネストを解除:
flatten([["a", "b"], ["c", "d"]]) # ["a", "b", "c", "d"]
sort() リストをソート:
sort(["c", "a", "b"]) # ["a", "b", "c"]
文字列関数
split() と join() 文字列とリスト間で変換:
split(",", "a,b,c") # ["a", "b", "c"]
join("-", ["a", "b"]) # "a-b"
upper() と lower() 大文字小文字を変更:
upper("hello") # "HELLO"
lower("WORLD") # "world"
replace() 部分文字列を置換:
replace("hello-world", "-", "_") # "hello_world"
trimspace() 空白を削除:
trimspace(" hello ") # "hello"
format() printf スタイルのフォーマット:
format("Instance %03d in %s", 42, "us-west-2")
# "Instance 042 in us-west-2"
条件分岐
三項演算子は便利です:
condition ? true_value : false_value
実際の使用例:
resource "aws_instance" "web" {
instance_type = var.environment == "prod" ? "t3.large" : "t3.micro"
tags = {
Environment = var.environment == "prod" ? "Production" : "Development"
}
}
チェーンすることもできますが、すぐに煩雑になります。代わりに locals を使用してください:
locals {
instance_type = (
var.environment == "prod" ? "t3.large" :
var.environment == "staging" ? "t3.medium" :
"t3.micro"
)
}
count を使った条件付きリソース:
resource "aws_cloudwatch_alarm" "cpu" {
count = var.enable_monitoring ? 1 : 0
# true なら 1 を作成、false なら 0 (リソースなし)
# ...
}
For 式とループ
For 式はコレクションを変換します。Python のリスト内包表記や JavaScript の map/filter のようなものです。
リストの変換
variable "names" {
default = ["alice", "bob", "charlie"]
}
locals {
# 大文字に変換
upper_names = [for name in var.names : upper(name)]
# ["ALICE", "BOB", "CHARLIE"]
# フィルタと変換
short_names = [for name in var.names : name if length(name) < 6]
# ["alice", "bob"]
}
マップの変換
variable "instances" {
default = {
web1 = "t3.micro"
web2 = "t3.small"
db = "t3.large"
}
}
locals {
# 値を変換
instance_sizes = {
for name, type in var.instances :
name => upper(type)
}
# { web1 = "T3.MICRO", web2 = "T3.SMALL", db = "T3.LARGE" }
# マップをフィルタ
web_instances = {
for name, type in var.instances :
name => type
if substr(name, 0, 3) == "web"
}
# { web1 = "t3.micro", web2 = "t3.small" }
}
オブジェクトから抽出
variable "servers" {
type = list(object({
name = string
size = string
}))
default = [
{ name = "web", size = "small" },
{ name = "db", size = "large" },
]
}
locals {
# 属性を抽出
server_names = [for s in var.servers : s.name]
# ["web", "db"]
# リストからマップを構築
server_map = {
for s in var.servers :
s.name => s.size
}
# { web = "small", db = "large" }
}
for_each によるリソースの反復
セットから複数のリソースを作成:
variable "buckets" {
type = set(string)
default = ["logs", "backups", "artifacts"]
}
resource "aws_s3_bucket" "app" {
for_each = var.buckets
bucket = "${var.project}-${each.key}"
tags = {
Name = each.value
}
}
# 参照: aws_s3_bucket.app["logs"].id
またはマップから:
variable "environments" {
type = map(object({
instance_type = string
instance_count = number
}))
default = {
dev = {
instance_type = "t3.micro"
instance_count = 1
}
prod = {
instance_type = "t3.large"
instance_count = 3
}
}
}
module "app" {
for_each = var.environments
source = "./modules/app"
environment = each.key
instance_type = each.value.instance_type
instance_count = each.value.instance_count
}
アイテムに一意の識別子がある場合 (マップキー、セット値) は for_each を使用してください。リソースはキーでアドレス指定されます: aws_instance.web["server1"]
数値インデックスによる単純な繰り返しには count を使用してください。リソースはインデックスでアドレス指定されます: aws_instance.web[0]
可能な限り for_each を優先してください。リストの途中からアイテムを削除する際、リソースが再番号付けされないため、より安全です。
すべてをまとめる
これまでカバーしたすべてを使って、実際のものを構築しましょう:
# variables.tf
variable "environment" {
type = string
default = "dev"
}
variable "allowed_ingress_rules" {
type = list(object({
port = number
protocol = string
cidr_blocks = list(string)
description = string
}))
default = [
{
port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "HTTP from anywhere"
},
{
port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "HTTPS from anywhere"
},
{
port = 22
protocol = "tcp"
cidr_blocks = ["10.0.0.0/8"]
description = "SSH from internal"
}
]
}
# main.tf
locals {
# 共通タグ
common_tags = {
Environment = var.environment
ManagedBy = "terraform"
Project = "web-app"
CostCenter = var.environment == "prod" ? "production" : "development"
}
# production で SSH をフィルタ
ingress_rules = [
for rule in var.allowed_ingress_rules :
rule
if !(var.environment == "prod" && rule.port == 22)
]
# for_each のためのルール名を生成
rule_names = {
for idx, rule in local.ingress_rules :
"${rule.protocol}-${rule.port}" => rule
}
}
resource "aws_security_group" "web" {
name = "${var.environment}-web-sg"
description = "Security group for ${var.environment} web servers"
tags = merge(
local.common_tags,
{ Name = "${var.environment}-web-sg" }
)
}
resource "aws_security_group_rule" "ingress" {
for_each = local.rule_names
security_group_id = aws_security_group.web.id
type = "ingress"
from_port = each.value.port
to_port = each.value.port
protocol = each.value.protocol
cidr_blocks = each.value.cidr_blocks
description = each.value.description
}
# outputs.tf
output "security_group_id" {
value = aws_security_group.web.id
description = "ID of the web security group"
}
output "allowed_ports" {
value = [for rule in local.ingress_rules : rule.port]
description = "List of allowed ingress ports"
}
何が起こっているか:
- 環境に基づく条件付きタグ
- ルールのフィルタリング (production では SSH なし)
for_eachのためにリストをマップに変換- データから動的リソースを作成
- 最終的なルールセットから計算された出力
これが infrastructure as real code です。設定ファイルではなく — 実際のプログラミングです。
理解度チェック
先に進む前に、以下の質問に答えてください:
- argument と expression の違いは何ですか?
list(string)とset(string)をいつ使い分けますか?- 1行でファイルを読み取って JSON として解析するにはどうしますか?
- 複数のリソースを管理する際、
for_eachがcountより安全な理由は? - キーが存在しない場合、
var.tags.Environmentにアクセスするとどうなりますか? - 5文字より長いアイテムのみを含むようにリストをフィルタするにはどうしますか?
- 条件分岐を含む複数行文字列の構文は何ですか?
- HCL でカスタム関数を定義できますか?
答え:
- Argument は
key = value; expression は値 (右側) list(string)は順序を保持し、重複を許可;set(string)は順序なしで一意性を強制jsondecode(file("config.json"))for_eachは安定したキーを使用;countはアイテムが削除されるとシフトするインデックスを使用- エラー。安全のため
lookup(var.tags, "Environment", "default")を使用 [for s in var.items : s if length(s) > 5]- heredoc 文字列内で
%{ if condition }...%{ endif }を使用 - いいえ。HCL は組み込み関数のみを提供。再利用性にはモジュールを使用
次のステップ
HCL の仕組みが理解できました。Part 5 では、この知識を使って variables と outputs で柔軟で再利用可能な設定を構築します。
学ぶこと:
- 入力変数の検証と制約
- センシティブな変数の処理
- 出力の依存関係とデータフロー
- 変数の優先順位とオーバーライド戦略
- 実際のシナリオのための複雑な変数構造
違いは何ですか? もう変数の例をコピーするだけではありません。型レベルで正しさを強制する変数スキーマを設計します。
連載ナビゲーション: