目次
- Variables, Outputs & State
- 📦 Code Examples
- 問題点: すべてがハードコード
- 解決策: Variables、Outputs、State
- Input Variables: ハードコードをやめる
- Validation: デプロイ前にエラーをキャッチ
- Variable Precedence: 値がどこから来るか
- Outputs: 情報を探し回るのをやめる
- State Management: Terraform のメモリ
- Sensitive Data: パスワードを漏らさない
- 問題
- 解決策 1: Variables を Sensitive としてマーク
- 解決策 2: 暗号化された Remote State
- 解決策 3: 外部 Secrets Management (ベストプラクティス)
- Real-World Pattern: Multi-Environment セットアップ
- Quick Check: 理解できましたか?
- 次は何?
- Resources
Variables, Outputs & State
初めての Terraform resource を書きました。コードは動きます。でも、すべてがハードコードされています。
同じインフラを staging と production にデプロイしたい?すべてをコピー&ペーストして手動で値を変更します。アプリの database endpoint が必要?インスタンスに SSH して探し回ります。クラウドアカウントに実際に何が存在するか知りたい?state ファイルがまだ残っていることを祈りましょう。
これではスケールしません。
この Part では、variables、outputs、state management を使って、柔軟で再利用可能なインフラを構築する方法を学びます。同じコードベースで複数の環境を管理。IP アドレスを探し回る必要はありません。「これ、もうデプロイしたっけ?」と悩むこともありません。
📦 Code Examples
Repository: terraform-hcl-tutorial-series 今回の Part: Part 5 - Variables and State
動作する例を取得:
git clone https://github.com/khuongdo/terraform-hcl-tutorial-series.git
cd terraform-hcl-tutorial-series
git checkout part-05
cd examples/part-05-variables-state/
# variable patterns を試してみる
terraform init
terraform plan -var-file="dev.tfvars"
問題点: すべてがハードコード
初心者の Terraform はこんな感じです:
# main.tf
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0" # staging で別の AMI を使いたい場合は?
instance_type = "t3.medium" # dev 環境で production サイズ?
tags = {
Name = "prod-web-server" # dev コードに "prod" をハードコード?
Environment = "production"
Owner = "alice@company.com" # Bob がデプロイする場合は?
}
}
resource "aws_db_instance" "main" {
engine = "postgres"
instance_class = "db.t3.large" # dev と prod で同じサイズ?
username = "admin"
password = "supersecret123" # これを Git にコミットしてませんよね?
}
何が問題なのでしょうか?
- 再利用性ゼロ (すべてを複製しないと staging にデプロイできない)
- 環境固有の値がコードに埋め込まれている
- インスタンスサイズを変更するにはコード変更が必要
- Git にパスワードが含まれている (セキュリティインシデントを楽しんでください)
- デプロイ後に database endpoint を取得する方法がない
実際のチームは数十の環境を管理します。このアプローチはすぐに破綻します。
解決策: Variables、Outputs、State
3つの概念がこの混乱を解決します:
- Input Variables: コードを柔軟にする
- Outputs: デプロイ後に重要な値を取得
- State: 実際に存在するものを追跡
このハードコードの災害を修正しましょう。
Input Variables: ハードコードをやめる
Variables はインフラをテンプレート化します。一度定義すれば、どこにでもデプロイできます。
基本的な Variables
# variables.tf
variable "instance_type" {
type = string
description = "EC2 instance type for web servers"
default = "t3.small"
}
variable "environment" {
type = string
description = "Deployment environment (dev, staging, prod)"
}
variable "enable_monitoring" {
type = bool
description = "Enable CloudWatch detailed monitoring"
default = false
}
それでは使ってみましょう:
# main.tf
resource "aws_instance" "web" {
ami = var.ami_id
instance_type = var.instance_type
monitoring = var.enable_monitoring
tags = {
Name = "${var.environment}-web-server"
Environment = var.environment
}
}
同じコード。環境ごとに異なる値。それがポイントです。
String 以外の Variable Types
Terraform は複雑な設定のために豊富な型をサポートしています:
# String
variable "region" {
type = string
default = "us-east-1"
}
# Number
variable "instance_count" {
type = number
default = 2
}
# Boolean
variable "enable_backups" {
type = bool
default = true
}
# List (順序付きコレクション)
variable "availability_zones" {
type = list(string)
default = ["us-east-1a", "us-east-1b", "us-east-1c"]
}
# Map (キー・バリュー ペア)
variable "instance_types" {
type = map(string)
default = {
dev = "t3.micro"
staging = "t3.small"
prod = "t3.large"
}
}
# Object (構造化データ)
variable "database_config" {
type = object({
engine = string
engine_version = string
instance_class = string
storage_gb = number
multi_az = bool
})
default = {
engine = "postgres"
engine_version = "15.4"
instance_class = "db.t3.small"
storage_gb = 20
multi_az = false
}
}
目的に応じて適切な型を使用します。順序付きデータには Lists、ルックアップには maps、構造化設定には objects を使います。
複雑な Variables の使用
# 環境ごとに異なる instance types
resource "aws_instance" "web" {
instance_type = var.instance_types[var.environment]
# dev -> t3.micro, prod -> t3.large
}
# availability zones にインスタンスを分散
resource "aws_subnet" "public" {
count = length(var.availability_zones)
availability_zone = var.availability_zones[count.index]
}
# 構造化された database configuration を使用
resource "aws_db_instance" "main" {
engine = var.database_config.engine
engine_version = var.database_config.engine_version
instance_class = var.database_config.instance_class
allocated_storage = var.database_config.storage_gb
multi_az = var.database_config.multi_az
}
Validation: デプロイ前にエラーをキャッチ
validation のない variables は危険です。誰かが誤って environment = "production123" と設定するでしょう。validation はクラウドアカウントに到達する前にそれをキャッチします。
基本的な Validation
variable "environment" {
type = string
description = "Deployment environment"
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}
environment = "production" で terraform plan を実行:
Error: Invalid value for variable
on variables.tf line 2:
2: variable "environment" {
Environment must be dev, staging, or prod.
デプロイ前にタイプミスをキャッチしました。それが目的です。
高度な Validation
# region format を検証
variable "region" {
type = string
validation {
condition = can(regex("^[a-z]{2}-[a-z]+-[0-9]$", var.region))
error_message = "Region must match format: us-east-1, eu-west-2, etc."
}
}
# instance count の範囲を検証
variable "instance_count" {
type = number
validation {
condition = var.instance_count >= 1 && var.instance_count <= 10
error_message = "Instance count must be between 1 and 10."
}
}
# CIDR block format を検証
variable "vpc_cidr" {
type = string
validation {
condition = can(cidrhost(var.vpc_cidr, 0))
error_message = "Must be a valid IPv4 CIDR block."
}
}
# クロスフィールド validation
variable "database_config" {
type = object({
instance_class = string
multi_az = bool
storage_gb = number
})
validation {
condition = (
var.database_config.multi_az == false ||
var.database_config.storage_gb >= 100
)
error_message = "Multi-AZ databases require at least 100GB storage."
}
}
Validation はドキュメントです。ユーザーにどの値が許容され、なぜそうなのかを正確に伝えます。
Variable Precedence: 値がどこから来るか
Terraform は複数のソースから variable 値を読み込みます。precedence を理解することで混乱を防ぎます。
Precedence チェーン (低から高)
variableblocks のデフォルト値- 環境変数 (
TF_VAR_name) terraform.tfvarsファイルterraform.tfvars.jsonファイル*.auto.tfvarsファイル (アルファベット順)-var-file=...コマンドラインフラグ-var=...コマンドラインフラグ
高い precedence が低いものを上書きします。CLI フラグが常に勝ちます。
Multi-Environment セットアップ
# ディレクトリ構造
terraform/
├── main.tf
├── variables.tf
├── terraform.tfvars # デフォルト値
├── environments/
│ ├── dev.tfvars # Development overrides
│ ├── staging.tfvars # Staging overrides
│ └── prod.tfvars # Production overrides
terraform.tfvars (デフォルト):
region = "us-east-1"
enable_monitoring = false
backup_retention_days = 7
environments/prod.tfvars (production overrides):
instance_type = "t3.large"
enable_monitoring = true
backup_retention_days = 30
multi_az = true
production にデプロイ:
terraform apply -var-file="environments/prod.tfvars"
同じコードベース、異なる設定。Netflix はこのパターンを使って 190 以上の国にデプロイしています。
Environment Variables
# environment 経由で variables を設定 (CI/CD で便利)
export TF_VAR_region="eu-west-1"
export TF_VAR_instance_count=5
terraform apply
# -var フラグを渡す必要なし
Command-Line Variables
# 個別の値を上書き
terraform apply \
-var="environment=staging" \
-var="instance_count=3"
CI/CD パイプラインでは environment variables を使用します。永続的な設定には .tfvars ファイルを使用します。
Outputs: 情報を探し回るのをやめる
インフラをデプロイした後、情報が必要です: database endpoints、load balancer URLs、IP addresses。Outputs がこれを解決します。
基本的な Outputs
# outputs.tf
output "instance_public_ip" {
description = "Public IP address of web server"
value = aws_instance.web.public_ip
}
output "database_endpoint" {
description = "RDS database connection endpoint"
value = aws_db_instance.main.endpoint
}
output "load_balancer_url" {
description = "Application load balancer URL"
value = "https://${aws_lb.main.dns_name}"
}
terraform apply 後:
Outputs:
instance_public_ip = "54.123.45.67"
database_endpoint = "mydb.abc123.us-east-1.rds.amazonaws.com:5432"
load_balancer_url = "https://my-alb-123456.us-east-1.elb.amazonaws.com"
コピー&ペースト準備完了。AWS Console で探し回る必要はありません。
Sensitive Outputs
output "database_password" {
description = "Master password for RDS instance"
value = aws_db_instance.main.password
sensitive = true
}
sensitive = true の場合:
terraform apply
Outputs:
database_password = <sensitive>
パスワードがログに漏れません。明示的に表示:
terraform output database_password
# 実際のパスワードを表示
常に secrets を sensitive としてマークします。CI/CD ログでの偶発的な露出を防ぎます。
構造化された Outputs
# 関連する outputs をグループ化
output "database_connection" {
description = "Complete database connection information"
value = {
endpoint = aws_db_instance.main.endpoint
port = aws_db_instance.main.port
database = aws_db_instance.main.db_name
username = aws_db_instance.main.username
}
sensitive = true
}
スクリプトでのアクセス:
terraform output -json database_connection | jq '.endpoint'
Module Composition のための Outputs
modules を構築する際 (Part 7)、outputs が module のインターフェースになります:
# networking module outputs
output "vpc_id" {
value = aws_vpc.main.id
}
output "public_subnet_ids" {
value = aws_subnet.public[*].id
}
application module で使用:
module "networking" {
source = "./modules/networking"
}
module "application" {
source = "./modules/application"
vpc_id = module.networking.vpc_id
subnet_ids = module.networking.public_subnet_ids
}
Outputs が modules を接続します。インフラレイヤー間の API です。
State Management: Terraform のメモリ
Terraform は state ファイルでインフラを追跡します。これにより、何が存在し、何が変更され、何を更新する必要があるかを把握します。
State とは?
terraform apply を実行すると、Terraform は:
.tfファイルから desired state を読み取るterraform.tfstateから current state を読み取る- それらを比較して plan を生成
- current = desired になるように変更を実行
state ファイルは Terraform のメモリです。これを失うと、Terraform はインフラの存在を忘れます。
State File の構造
EC2 インスタンスをデプロイした後、terraform.tfstate には以下が含まれます:
{
"version": 4,
"terraform_version": "1.7.0",
"serial": 12,
"lineage": "a1b2c3d4-e5f6-7890-abcd-1234567890ab",
"outputs": {
"instance_ip": {
"value": "54.123.45.67",
"type": "string"
}
},
"resources": [
{
"mode": "managed",
"type": "aws_instance",
"name": "web",
"provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
"instances": [
{
"schema_version": 1,
"attributes": {
"id": "i-0abcd1234efgh5678",
"ami": "ami-0c55b159cbfafe1f0",
"instance_type": "t3.medium",
"public_ip": "54.123.45.67",
"private_ip": "10.0.1.42"
}
}
]
}
]
}
主要な要素:
- version: State ファイル形式のバージョン
- serial: 変更ごとに増加 (並行変更を検出)
- lineage: state ファイルの混同を防ぐユニーク ID
- outputs: キャッシュされた output 値
- resources: 完全な resource 属性 (IDs、IPs、ARNs)
Local vs Remote State
Local state (デフォルト):
# state は current directory の terraform.tfstate に保存
terraform apply
# ./terraform.tfstate を作成/更新
local state の問題点:
- コラボレーションなし (チームメンバーが state を共有できない)
- ロックなし (並行
terraform applyが破損を引き起こす) - バックアップなし (ファイルを削除 = インフラ追跡を失う)
- secrets が平文 (state にパスワード、キーが含まれる)
Remote state (production アプローチ):
# backend.tf
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks" # State locking
}
}
メリット:
- 共有アクセス (チームが同じ state を使用)
- ロック (DynamoDB が並行変更を防止)
- 暗号化 (state が保存時に暗号化)
- バージョニング (S3 バージョニングでロールバック可能)
Part 9 で remote state backends をカバーします。今のところ: production では local state を使用しないでください。
State Locking
2人のエンジニアが同時に terraform apply を実行したとします:
- 両方が current state を読む: "2 instances が存在"
- 両方が 1 つ追加の instance を作成する計画
- 両方が変更を適用
- 結果: 3 ではなく 4 instances (競合状態)
state locking がこれを防ぎます:
# Engineer 1
terraform apply
Acquiring state lock. This may take a few moments...
Lock acquired (ID: abc-123)
# Engineer 2 (同時に)
terraform apply
Acquiring state lock. This may take a few moments...
Error: Error locking state: ConditionalCheckFailedException
Lock Info:
ID: abc-123
Operation: OperationTypeApply
Who: engineer1@laptop
Created: 2026-03-12 14:32:15 UTC
Engineer 2 の apply は Engineer 1 が終了するまでブロックされます。競合状態はありません。
Sensitive Data: パスワードを漏らさない
Terraform state にはインフラに関するすべてが含まれ、secrets も含まれます。慎重に取り扱ってください。
問題
resource "aws_db_instance" "main" {
password = "supersecret123" # これはやめましょう
}
terraform apply 後、terraform.tfstate には以下が含まれます:
{
"resources": [{
"instances": [{
"attributes": {
"password": "supersecret123" # state に平文
}
}]
}]
}
state ファイルにアクセスできる人は誰でもデータベースパスワードを持っています。
解決策 1: Variables を Sensitive としてマーク
variable "db_password" {
type = string
sensitive = true
}
Terraform はログの sensitive 値を隠します:
terraform plan
# aws_db_instance.main will be created
+ resource "aws_db_instance" "main" {
+ password = (sensitive value)
}
しかし state には平文が含まれます。これはログ漏洩を防ぐだけです。
解決策 2: 暗号化された Remote State
terraform {
backend "s3" {
bucket = "my-state-bucket"
key = "terraform.tfstate"
encrypt = true # state を保存時に暗号化
# 追加のセキュリティのための KMS 暗号化
kms_key_id = "arn:aws:kms:us-east-1:123456789:key/abc-123"
}
}
state は S3 で暗号化されます。読み取るには AWS KMS 権限が必要です。
解決策 3: 外部 Secrets Management (ベストプラクティス)
Terraform に secrets を保存しないでください。実行時に取得します:
# AWS Secrets Manager からパスワードを取得
data "aws_secretsmanager_secret_version" "db_password" {
secret_id = "prod/database/master-password"
}
resource "aws_db_instance" "main" {
password = data.aws_secretsmanager_secret_version.db_password.secret_string
}
secret は Secrets Manager に保存され、Git にはありません。state にはまだ含まれますが、保存時に暗号化されます。
Part 11 で HashiCorp Vault 統合をカバーします。今のところ: secrets は secret stores に属し、コードには属しません。
Real-World Pattern: Multi-Environment セットアップ
すべてをまとめてみましょう:
terraform/
├── main.tf # Core infrastructure
├── variables.tf # Variable declarations
├── outputs.tf # Output definitions
├── backend.tf # Remote state configuration
├── terraform.tfvars # Default values
└── environments/
├── dev.tfvars # Development overrides
├── staging.tfvars # Staging overrides
└── prod.tfvars # Production overrides
variables.tf:
variable "environment" {
type = string
description = "Deployment environment"
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Must be dev, staging, or prod."
}
}
variable "instance_config" {
type = object({
type = string
count = number
})
description = "EC2 instance configuration"
validation {
condition = var.instance_config.count >= 1 && var.instance_config.count <= 20
error_message = "Instance count must be 1-20."
}
}
variable "enable_multi_az" {
type = bool
description = "Enable multi-AZ deployment for high availability"
default = false
}
main.tf:
resource "aws_instance" "web" {
count = var.instance_config.count
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_config.type
tags = {
Name = "${var.environment}-web-${count.index + 1}"
Environment = var.environment
}
}
resource "aws_db_instance" "main" {
engine = "postgres"
instance_class = var.environment == "prod" ? "db.t3.large" : "db.t3.small"
multi_az = var.enable_multi_az
# Secrets Manager からのパスワード
password = data.aws_secretsmanager_secret_version.db_password.secret_string
}
outputs.tf:
output "web_server_ips" {
description = "Public IPs of web servers"
value = aws_instance.web[*].public_ip
}
output "database_endpoint" {
description = "Database connection endpoint"
value = aws_db_instance.main.endpoint
sensitive = true
}
environments/prod.tfvars:
environment = "prod"
instance_config = {
type = "t3.large"
count = 5
}
enable_multi_az = true
production にデプロイ:
terraform apply -var-file="environments/prod.tfvars"
Plan: 6 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
Apply complete! Resources: 6 added, 0 changed, 0 destroyed.
Outputs:
web_server_ips = [
"54.123.45.67",
"54.123.45.68",
"54.123.45.69",
"54.123.45.70",
"54.123.45.71"
]
database_endpoint = <sensitive>
dev、staging、prod で同じコード。variables だけが異なります。
Quick Check: 理解できましたか?
Part 6 に進む前に、これらの質問に答えられますか?
list(string)とmap(string)の違いは?- Lists は順序付き、インデックスでアクセス (
[0]、[1])。Maps はキー・バリューペア、キーでアクセス (["dev"]、["prod"])。
- Lists は順序付き、インデックスでアクセス (
どちらが勝つ:
terraform.tfvarsvs-varCLI フラグ?- CLI フラグが最高の precedence を持ちます。
-varはすべてを上書きします。
- CLI フラグが最高の precedence を持ちます。
なぜ outputs を
sensitive = trueとしてマーク?- secrets がコンソール出力と CI/CD ログに表示されるのを防ぎます。表示するには明示的に
terraform output <name>を強制します。
- secrets がコンソール出力と CI/CD ログに表示されるのを防ぎます。表示するには明示的に
state ファイルを失った場合どうなる?
- Terraform はインフラの存在を忘れます。次の
applyはすべてを再作成しようとし、競合を引き起こします。
- Terraform はインフラの存在を忘れます。次の
production secrets はどこに保存すべき?
- 外部 secret stores (AWS Secrets Manager、HashiCorp Vault)。実行時に
datasources 経由で参照します。
- 外部 secret stores (AWS Secrets Manager、HashiCorp Vault)。実行時に
これらの答えに自信があれば、Part 6 の準備ができています。
次は何?
Part 6: Core Terraform Workflow では、開発サイクルをマスターします:
terraform init: providers と modules を初期化terraform plan: apply 前に変更をプレビューterraform apply: インフラを安全にデプロイterraform destroy: リソースをクリーンに削除terraform refresh: state を現実と同期terraform taint: リソースの再作成を強制
さらに、drift の処理、障害からの回復、plan のデバッグのワークフローも。
Terraform 開発ループを学ぶ準備はできましたか? Part 6 へ続く → (coming soon)
Resources
- Terraform Variables Documentation
- Terraform Outputs Documentation
- Terraform State Documentation
- AWS Secrets Manager Terraform Provider
連載ナビゲーション:
- 第1回: Infrastructure as Codeとは?
- 第2回: Terraformのセットアップ
- 第3回: 初めてのクラウドリソース
- 第4回: HCL基礎
- 第5回: Variables, Outputs & State (今ここ)
- 第6回: Terraformの基本ワークフロー
- 第7回: モジュールで整理する (近日公開)
- 第8回: マルチクラウドパターン (近日公開)
- 第9回: State管理 & チームワークフロー (近日公開)
- 第10回: テスト & 検証 (近日公開)
- 第11回: セキュリティ & シークレット管理 (近日公開)
- 第12回: 本番環境パターン & DevSecOps (近日公開)
この記事は "Terraform from Fundamentals to Production" シリーズの一部です。