resourceブロック — Terraformでインフラリソースを定義する基本ブロック

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.publicaws_vpc.main.idを参照しているため、Terraformは「VPCを先に作ってからサブネットを作る」という順序を自動的に把握します。明示的に順序を指定する必要はありません。

この自動解決が機能しないケースではdepends_onメタ引数を使います(詳細は「depends_onの使い方」を参照)。

メタ引数

メタ引数は、どのリソースタイプにも使える特殊な引数です。

メタ引数概要詳細記事
count同じリソースを整数個作成するcountの使い方
for_eachmapまたは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. 関連記事


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