1. 概要
この記事では、以下の内容を解説します。
resourceブロックの基本構文と各要素の意味- リソースが持つ引数(Arguments)と属性(Attributes)の違い
<TYPE>.<NAME>.<ATTR>形式の属性参照- Terraformがリソース間の依存関係を自動解決する仕組み
- EC2・S3・IAM・Security Groupを使った実践的なコード例
- よくあるエラーとその解決方法
resourceブロックはTerraformの最も重要なブロックです。「どのリソースを作るか」を定義するブロックであり、Terraformのコードのほとんどはresourceブロックで構成されます。
2. Terraformにおける位置付け
resourceブロックとは
resourceブロックは、AWSのEC2インスタンスやS3バケット、VPCといった実際のインフラリソースを定義するブロックです。
Terraformを使う主な目的は「インフラリソースを作成・変更・削除すること」であり、その定義をするのがresourceブロックです。
terraform.tf ファイル内の resource ブロック
↓ terraform apply
AWSリソースの作成・変更・削除
↓
terraform.tfstate に状態を記録
どんな場面で使うか
- EC2インスタンス、ALB、RDSなどのAWSリソースを作成するとき
- VPC、サブネット、セキュリティグループなどのネットワーク設定をするとき
- S3バケット、IAMロール、CloudWatchアラームを定義するとき
Terraformで何かリソースを作るときは、必ずresourceブロックを使います。
3. 基本構文
resourceブロックの形式
resource "<リソースタイプ>" "<ローカル名>" {
引数名 = 値
引数名 = 値
}
| 要素 | 説明 | 例 |
|---|---|---|
resource | ブロックタイプ(固定) | resource |
<リソースタイプ> | 作成するリソースの種類 | "aws_instance" |
<ローカル名> | このTerraformコード内での識別名 | "web" |
| 引数 | リソースの設定値 | instance_type = "t3.micro" |
最もシンプルな例
resource "aws_s3_bucket" "logs" {
bucket = "my-application-logs-2026"
}
- リソースタイプ:
aws_s3_bucket(AWSのS3バケット) - ローカル名:
logs(このTerraformコード内での名前) bucket: バケット名を指定する引数
リソースアドレス
Terraformでは、リソースを<リソースタイプ>.<ローカル名>の形式で識別します。これをリソースアドレスと呼びます。
aws_s3_bucket.logs ← リソースアドレス
aws_instance.web ← リソースアドレス
aws_iam_role.app ← リソースアドレス
terraform stateコマンドやエラーメッセージでも、このアドレス形式が使われます。
4. 詳細解説
引数(Arguments)と属性(Attributes)の違い
resourceブロックの中には2種類の情報があります。
引数(Arguments): ユーザーがリソースに設定する入力値です。コードの中で定義します。
resource "aws_instance" "web" {
ami = "..." # 引数: ユーザーが指定する
instance_type = "t3.micro" # 引数: ユーザーが指定する
}
属性(Attributes): リソースが作成された後にAWSから返ってくる値です。terraform applyを実行して初めて確定します。
# 以下はすべて属性(apply後に確定する)
aws_instance.web.id # インスタンスID(i-0abc123...)
aws_instance.web.public_ip # パブリックIPアドレス
aws_instance.web.private_ip # プライベートIPアドレス
aws_instance.web.arn # ARN
属性参照(<TYPE>.<NAME>.<ATTR>)
あるリソースの属性を別のリソースの引数として使う場合は、<リソースタイプ>.<ローカル名>.<属性名>の形式で参照します。
# VPCを作成
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
}
# サブネットを作成(VPCのIDを参照)
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id # ← 属性参照
cidr_block = "10.0.1.0/24"
}
aws_vpc.main.idは「aws_vpcタイプで名前がmainのリソースのid属性」という意味です。
自動的な依存関係の解決
属性参照を使うと、Terraformは自動的にリソースの作成順序を決定します。
上記の例では、aws_subnet.publicがaws_vpc.main.idを参照しているため、Terraformは「VPCを先に作ってからサブネットを作る」という順序を自動的に把握します。明示的に順序を指定する必要はありません。
この自動解決が機能しないケースではdepends_onメタ引数を使います(詳細は「depends_onの使い方」を参照)。
メタ引数
メタ引数は、どのリソースタイプにも使える特殊な引数です。
| メタ引数 | 概要 | 詳細記事 |
|---|---|---|
count | 同じリソースを整数個作成する | countの使い方 |
for_each | mapまたはsetを使って動的にリソースを作成する | for_eachの使い方 |
depends_on | 明示的な依存関係を宣言する | depends_onの使い方 |
lifecycle | リソースの作成・更新・削除の動作を制御する | lifecycleの使い方 |
provider | リソースに使用するプロバイダーのエイリアスを指定する | providerの設定 |
5. 実践例
例1: EC2インスタンスの作成(基本)
VPC・サブネット・セキュリティグループ・EC2インスタンスを作成する基本的な構成です。
# providers.tf
terraform {
required_version = ">= 1.9"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "ap-northeast-1"
default_tags {
tags = {
ManagedBy = "terraform"
Environment = "dev"
}
}
}
# main.tf
# ────────────────────────────────────────
# データソース: Amazon Linux 2023の最新AMIを動的に取得
# ────────────────────────────────────────
data "aws_ami" "amazon_linux_2023" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-*-x86_64"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
# ────────────────────────────────────────
# VPC
# ────────────────────────────────────────
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
tags = { Name = "dev-vpc" }
}
# ────────────────────────────────────────
# パブリックサブネット(ap-northeast-1a)
# ────────────────────────────────────────
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id # VPCのIDを参照
cidr_block = "10.0.1.0/24"
availability_zone = "ap-northeast-1a"
map_public_ip_on_launch = true
tags = { Name = "dev-public-subnet" }
}
# ────────────────────────────────────────
# インターネットゲートウェイ
# ────────────────────────────────────────
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = { Name = "dev-igw" }
}
# ────────────────────────────────────────
# ルートテーブル(パブリック)
# ────────────────────────────────────────
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id # IGWのIDを参照
}
tags = { Name = "dev-public-rt" }
}
# ルートテーブルとサブネットの関連付け。
# Terraformではルートテーブルの定義(aws_route_table)と
# サブネットへの割り当て(aws_route_table_association)が別々のリソースになる。
resource "aws_route_table_association" "public" {
subnet_id = aws_subnet.public.id # サブネットのIDを参照
route_table_id = aws_route_table.public.id # ルートテーブルのIDを参照
}
# ────────────────────────────────────────
# セキュリティグループ(SSH不要: SSMで接続)
# ────────────────────────────────────────
resource "aws_security_group" "web" {
name = "dev-web-sg"
description = "Security group for web servers"
vpc_id = aws_vpc.main.id
ingress {
description = "HTTPS from anywhere"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = { Name = "dev-web-sg" }
lifecycle {
create_before_destroy = true
}
}
# ────────────────────────────────────────
# IAMロール(SSM接続用)
# ────────────────────────────────────────
# aws_iam_policy_document: IAMポリシーをHCLの構造体で記述し、
# jsonencode()なしでJSON形式の信頼ポリシーを生成するデータソース。
# インラインJSONより見やすく、変数や条件を組み込みやすい。
data "aws_iam_policy_document" "ec2_assume" {
statement {
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["ec2.amazonaws.com"]
}
}
}
resource "aws_iam_role" "web" {
name = "dev-web-role"
assume_role_policy = data.aws_iam_policy_document.ec2_assume.json
}
resource "aws_iam_role_policy_attachment" "ssm" {
role = aws_iam_role.web.name
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
# IAMインスタンスプロファイル:
# IAMロールをEC2インスタンスに付与するためのコンテナ。
# EC2はIAMロールを直接受け取れないため、インスタンスプロファイル経由で渡す。
# 1つのIAMロールにつき1つのインスタンスプロファイルを作成する。
resource "aws_iam_instance_profile" "web" {
name = "dev-web-profile"
role = aws_iam_role.web.name
}
# ────────────────────────────────────────
# EC2インスタンス
# ────────────────────────────────────────
resource "aws_instance" "web" {
ami = data.aws_ami.amazon_linux_2023.id
instance_type = "t3.micro"
subnet_id = aws_subnet.public.id
vpc_security_group_ids = [aws_security_group.web.id]
iam_instance_profile = aws_iam_instance_profile.web.name
root_block_device {
volume_type = "gp3"
volume_size = 20
encrypted = true
delete_on_termination = true
}
tags = {
Name = "dev-web-server"
Role = "web"
}
}
# outputs.tf
output "instance_id" {
description = "EC2インスタンスID"
value = aws_instance.web.id
}
output "ssm_connect" {
description = "SSMでEC2に接続するコマンド"
value = "aws ssm start-session --target ${aws_instance.web.id} --region ap-northeast-1"
}
例2: S3バケット(アクセスコントロール設定付き)
# main.tf
# S3バケット本体
resource "aws_s3_bucket" "app_data" {
bucket = "myapp-dev-data-2026"
tags = {
Name = "myapp-dev-data"
Purpose = "Application data storage"
}
}
# パブリックアクセスを全てブロック(デフォルトで有効化推奨)
resource "aws_s3_bucket_public_access_block" "app_data" {
bucket = aws_s3_bucket.app_data.id # S3バケットのIDを参照
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
# バージョニングを有効化
resource "aws_s3_bucket_versioning" "app_data" {
bucket = aws_s3_bucket.app_data.id
versioning_configuration {
status = "Enabled"
}
}
# サーバーサイド暗号化を有効化(AES-256)
resource "aws_s3_bucket_server_side_encryption_configuration" "app_data" {
bucket = aws_s3_bucket.app_data.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
💡 ポイント: S3バケットのパブリックアクセスブロック・バージョニング・暗号化は別々のリソースとして定義します。これはAWS Providerのバージョン4.0以降の変更で、以前は
aws_s3_bucketの引数として設定できましたが、現在は分離されています。
例3: CloudWatch アラーム(EC2 CPU使用率の監視)
# main.tf(例1のEC2インスタンスを前提)
# EC2インスタンスのCPU使用率が80%を超えたらアラート
resource "aws_cloudwatch_metric_alarm" "cpu_high" {
alarm_name = "dev-web-cpu-high"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 2
metric_name = "CPUUtilization"
namespace = "AWS/EC2"
period = 300 # 5分
statistic = "Average"
threshold = 80
alarm_description = "EC2 CPU使用率が80%を超えました"
# EC2インスタンスIDを参照してアラームを紐付け
dimensions = {
InstanceId = aws_instance.web.id
}
# 通知先のSNSトピック(別途作成が必要)
alarm_actions = [aws_sns_topic.alerts.arn]
tags = {
Name = "dev-web-cpu-high"
}
}
# 通知用SNSトピック
resource "aws_sns_topic" "alerts" {
name = "dev-alerts"
}
# メール通知を受け取る場合は aws_sns_topic_subscription リソースを追加する
# resource "aws_sns_topic_subscription" "alerts_email" {
# topic_arn = aws_sns_topic.alerts.arn
# protocol = "email"
# endpoint = "your-email@example.com" # サブスクリプション確認メールが届く
# }
例4: Systems Manager Parameter Store(設定値の管理)
環境変数やDB接続文字列などをパラメータストアで管理するパターンです。
# main.tf
# 通常の文字列パラメータ(アプリケーションの設定値)
resource "aws_ssm_parameter" "app_env" {
name = "/myapp/dev/APP_ENV"
type = "String"
value = "development"
tags = { Name = "myapp-dev-app-env" }
}
# 機密文字列パラメータ(DBパスワード等)
resource "aws_ssm_parameter" "db_password" {
name = "/myapp/dev/DB_PASSWORD"
type = "SecureString"
value = var.db_password # variableから値を受け取る(ハードコード禁止)
key_id = "alias/aws/ssm" # KMSキーエイリアス
tags = { Name = "myapp-dev-db-password" }
lifecycle {
ignore_changes = [value] # Terraform外での変更を無視する
}
}
6. よくあるエラー
Error: Missing required argument
│ Error: Missing required argument
│
│ on main.tf line 3, in resource "aws_instance" "web":
│ 3: resource "aws_instance" "web" {
│
│ The argument "ami" is required, but no definition was found.
原因: aws_instanceにはami引数が必須だが、指定されていない。
解決方法:
resource "aws_instance" "web" {
ami = data.aws_ami.amazon_linux_2023.id # 必須引数
instance_type = "t3.micro" # 必須引数
}
Error: Reference to undeclared resource
│ Error: Reference to undeclared resource
│
│ on main.tf line 10, in resource "aws_subnet" "public":
│ 10: vpc_id = aws_vpc.network.id
│
│ A managed resource "aws_vpc" "network" has not been declared in the root module.
原因: 参照しているリソースが存在しない。リソースタイプ名またはローカル名が間違っている。
解決方法: 参照先のリソースアドレスを確認する。
# NG: 存在しないリソースを参照
vpc_id = aws_vpc.network.id # "network"という名前のVPCがない
# OK: 正しいローカル名で参照
vpc_id = aws_vpc.main.id # "main"という名前のVPCが存在する
Error: Cycle detected(循環依存)
│ Error: Cycle: aws_security_group.web, aws_instance.web
原因: 2つ以上のリソースが互いに参照し合っている(循環参照)。
解決方法: 参照関係を整理し、どちらか一方の参照をなくす。どうしても相互参照が必要な場合はdepends_onで順序を明示する。
Error: Instance cannot be destroyed(prevent_destroy)
│ Error: Instance cannot be destroyed
│
│ on main.tf line 3, in resource "aws_instance" "web":
│ 3: resource "aws_instance" "web" {
│
│ Resource aws_instance.web has lifecycle.prevent_destroy set,
│ but the plan calls for this resource to be destroyed.
原因: lifecycle { prevent_destroy = true } が設定されているリソースを削除しようとしている。
解決方法: prevent_destroy = false に変更するか、lifecycleブロックを削除してから再度terraform destroyを実行する。
エラー: -/+ 置換が意図せず発生する
terraform planの出力に-/+(削除後に再作成)が表示されることがあります。これはリソースが一度削除されてから新しく作られることを意味します。
# aws_instance.web must be replaced
-/+ resource "aws_instance" "web" {
~ ami = "ami-old" -> "ami-new" # forces replacement
# forces replacementと表示されている引数が置換の原因です。変更したくない場合はignore_changesで対応できます。
resource "aws_instance" "web" {
# ...
lifecycle {
ignore_changes = [ami] # AMIの変更差分を無視する
}
}
7. ベストプラクティス
推奨パターン
リソースのローカル名は役割を表す名前にする
Terraformのローカル名(resource "aws_instance" "ここ"の部分)は、リソースの役割を表す名前にします。
# 推奨
resource "aws_instance" "web" {} # Webサーバー
resource "aws_instance" "bastion" {} # 踏み台サーバー
resource "aws_s3_bucket" "access_logs" {} # アクセスログ用
# 非推奨(役割が不明)
resource "aws_instance" "this" {}
resource "aws_instance" "instance1" {}
属性参照を積極的に使う
リソース間の値は属性参照で連携します。ID等の値をハードコードすると、Terraformの依存関係解決が機能せず、バグの原因になります。
# NG: セキュリティグループIDをハードコード
vpc_security_group_ids = ["sg-0abc12345"]
# OK: 属性参照で連携
vpc_security_group_ids = [aws_security_group.web.id]
lifecycleブロックを適切に使う
本番環境の重要なリソースにはprevent_destroy = trueを設定し、誤削除を防ぎます。
resource "aws_rds_cluster" "main" {
# ...
lifecycle {
prevent_destroy = true # 誤削除防止
}
}
S3バケットはパブリックアクセスブロックを必ず設定する
S3バケットを作成したら、必ずaws_s3_bucket_public_access_blockリソースでパブリックアクセスをブロックします。
避けるべきパターン
同じリソースタイプを大量にコピーして書く
同じようなリソースを複数作る場合は、コードのコピーではなくfor_eachを使います。
# NG: コードの重複
resource "aws_s3_bucket" "bucket_a" { bucket = "bucket-a" }
resource "aws_s3_bucket" "bucket_b" { bucket = "bucket-b" }
resource "aws_s3_bucket" "bucket_c" { bucket = "bucket-c" }
# OK: for_each で動的に作成
resource "aws_s3_bucket" "buckets" {
for_each = toset(["bucket-a", "bucket-b", "bucket-c"])
bucket = each.key
}
# for_each で作ったリソースは [キー名] で参照する
# 例: aws_s3_bucket.buckets["bucket-a"].id
# 詳細は「for_eachの使い方」を参照
AMI IDのハードコード
AMI IDはリージョンごと・更新ごとに変わります。dataソースで動的に取得することを推奨します。
8. 関連記事
- for_eachの使い方 — 複数リソースを動的に作成する
- countの使い方 — 整数でリソースを複数作成する
- lifecycleの使い方 — リソースのライフサイクル制御
- depends_onの使い方 — 明示的な依存関係の設定
- dataブロックの使い方 — 既存リソースを参照する
- variable(入力変数)の使い方 — 引数を変数で管理する
- count vs for_eachの違い — 複数作成時の使い分け
9. まとめ
resourceブロックはTerraformの最も重要なブロックで、作成するインフラを定義するresource "<リソースタイプ>" "<ローカル名>"の形式で記述する- 引数(Arguments)はユーザーが設定する入力値で、属性(Attributes)はapply後にAWSから返ってくる値
<TYPE>.<NAME>.<ATTR>形式で別リソースの属性を参照でき、これによりTerraformが依存関係を自動解決するcount/for_each/depends_on/lifecycleの4つのメタ引数で動作を制御できる- ローカル名は役割が分かる名前にし、値の参照はハードコードせず属性参照を使う
動作確認バージョン: Terraform >= 1.9 / AWS Provider ~> 5.0 対象リージョン: ap-northeast-1(東京) 公式ドキュメント: https://developer.hashicorp.com/terraform/language/resources/syntax