★ 初級

03. EC2 とコンピュート

AWS の代表サービス EC2(Elastic Compute Cloud)。仮想マシンを 1 台立てるだけのシンプル例から、AMI を最新で取得する/起動スクリプトを流す/IAM ロールを付ける、までを順に。

最小例

# EC2 を 1 台立ち上げる最小コード(VPC/SG 未指定 → デフォルト VPC が使われる)
# ami と instance_type の 2 つだけが事実上必須
resource "aws_instance" "web" {
  ami           = "ami-0c4a35bf6c1f8c39d"   # AMI ID(リージョン固有・要置き換え)
  instance_type = "t3.micro"                 # サイズ(vCPU/メモリの組合せ)

  tags = {
    Name = "web"
  }
}

これだけで EC2 1 台が起動します。ただしデフォルト VPC の中、デフォルト SG という不便な構成。実用には次のように VPC や SG を指定します。

# 実用的な EC2 設定: VPC 内に配置し SG / 鍵 / IAM ロールを指定
resource "aws_instance" "web" {
  ami                    = data.aws_ami.al2023.id           # AMI を data で取得(次節)
  instance_type          = "t3.micro"
  subnet_id              = aws_subnet.public["a"].id        # どの subnet に置くか
  vpc_security_group_ids = [aws_security_group.web.id]      # 適用する SG(list)
  key_name               = aws_key_pair.deployer.key_name   # SSH 公開鍵
  iam_instance_profile   = aws_iam_instance_profile.ec2.name # この EC2 の AWS 権限

  tags = { Name = "web" }
}

AMI を最新で取得(data ソース)

AMI ID をコードに固定で書くと、月が変わると古い AMI のまま起動し続けて、セキュリティパッチが当たりません。data "aws_ami" で「最新の Amazon Linux 2023」を毎回引いてくるのが定石。

# Amazon Linux 2023 の最新 AMI を毎回検索(ID ハードコード回避)
# 2 つの filter で「名前パターン × 仮想化タイプ」を絞り込み
data "aws_ami" "al2023" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["al2023-ami-*-x86_64"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}

# data で取得した AMI ID をそのまま EC2 の ami に渡す
resource "aws_instance" "web" {
  ami           = data.aws_ami.al2023.id
  instance_type = "t3.micro"
  # ...
}
terraform plan 時の挙動 data ソースは plan のたびに最新を引いてくるので、AMI が更新されると差分が出ます。「AMI が更新されたから再作成しろ」と Terraform が言ってきます。これを避けたい時は lifecycle.ignore_changes = [ami] を使う。

user_data で起動時スクリプト

EC2 起動時に 1 度だけ流すシェルスクリプト。

# user_data = EC2 起動時に 1 度だけ実行されるシェルスクリプト
# ヒアドキュメント (<<-EOT) で複数行を書ける
resource "aws_instance" "web" {
  ami           = data.aws_ami.al2023.id
  instance_type = "t3.micro"

  user_data = <<-EOT
    #!/bin/bash
    dnf update -y
    dnf install -y nginx
    systemctl enable --now nginx
    echo "Hello from $(hostname)" > /usr/share/nginx/html/index.html
  EOT

  # user_data を変更したら EC2 を作り直す(変更を確実に反映)
  user_data_replace_on_change = true
}

テンプレ化するなら templatefile():

# 別ファイルに user_data を切り出して、変数を差し込んでレンダリングする例
# templates/web-init.sh.tpl の中の ${name} が var.environment の値で置き換わる
# # templates/web-init.sh.tpl の中身:
# # #!/bin/bash
# # echo "Hello, ${name}!" > /tmp/hello.txt

resource "aws_instance" "web" {
  user_data = templatefile("${path.module}/templates/web-init.sh.tpl", {
    name = var.environment   # ← テンプレ内の ${name} に展開される
  })
}

IAM ロールを付ける

EC2 から AWS API を叩くには IAM ロール を「インスタンスプロファイル」経由で attach します。詳細は 05 章。最小はこれ:

# EC2 用 IAM ロール: assume_role_policy で「EC2 サービスがこのロールを assume できる」と宣言
# jsonencode() で HCL の map を JSON 文字列に変換
resource "aws_iam_role" "ec2" {
  name = "ec2-web"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Allow"
      Principal = { Service = "ec2.amazonaws.com" }
      Action = "sts:AssumeRole"
    }]
  })
}

# AWS 管理ポリシー(SSM 用)を上のロールにアタッチ
# SSM Session Manager で SSH なしで EC2 に入れるようになる
resource "aws_iam_role_policy_attachment" "ssm" {
  role       = aws_iam_role.ec2.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}

# IAM Instance Profile = IAM ロールを EC2 にアタッチするためのラッパー(EC2 特有)
# EC2 から IAM ロールを「直接」ではなく Instance Profile 経由で参照する必要がある
resource "aws_iam_instance_profile" "ec2" {
  name = "ec2-web"
  role = aws_iam_role.ec2.name
}

# EC2 に Instance Profile を割り当て
resource "aws_instance" "web" {
  # ...
  iam_instance_profile = aws_iam_instance_profile.ec2.name
}
SSM 経由で SSH レス運用 上の AmazonSSMManagedInstanceCore を付けると、SSH キーや 22 番ポート開放なしで aws ssm start-session --target i-xxx で入れるようになります。22 番を開けない がベストプラクティス。

ディスク設定(root_block_device)

# EC2 のディスク詳細設定
# root_block_device: ルートディスク(OS が入る)の設定
# ebs_block_device: 追加のデータディスク(必要に応じて 1 つ以上付ける)
resource "aws_instance" "web" {
  ami           = data.aws_ami.al2023.id
  instance_type = "t3.medium"

  root_block_device {
    volume_size           = 30          # GB
    volume_type           = "gp3"        # 推奨(gp2 より安く速い)
    iops                  = 3000        # gp3 は IOPS を独立に設定可能
    throughput            = 125          # MB/s
    encrypted             = true         # 暗号化(本番では必須)
    delete_on_termination = true         # EC2 終了時にディスクも削除
  }

  # 追加データディスクをアタッチする例
  ebs_block_device {
    device_name = "/dev/sdf"   # OS 内で /dev/nvme1n1 等として見える
    volume_size = 100
    volume_type = "gp3"
    encrypted   = true
  }
}

複数台を一括(for_each)

# 複数台の EC2 を「役割が違う」設定で一括作成する例
# まず map で「キー = サーバ名、値 = 設定」を定義
locals {
  servers = {
    api    = { type = "t3.small", subnet = "a" }
    worker = { type = "t3.micro", subnet = "c" }
    cron   = { type = "t3.nano",  subnet = "a" }
  }
}

# for_each で map の各エントリを 1 つの EC2 に展開
# 結果: aws_instance.fleet["api"], ["worker"], ["cron"] の 3 リソース
resource "aws_instance" "fleet" {
  for_each = local.servers

  ami           = data.aws_ami.al2023.id
  instance_type = each.value.type   # 各サーバの type を参照
  subnet_id     = aws_subnet.private[each.value.subnet].id   # subnet も各設定から

  vpc_security_group_ids = [aws_security_group.app.id]
  iam_instance_profile   = aws_iam_instance_profile.ec2.name

  tags = { Name = each.key }   # api / worker / cron
}

# for_each のリソースは map のキーでアクセス
output "api_private_ip" {
  value = aws_instance.fleet["api"].private_ip
}

EC2 以外の選択肢

用途EC2 ではなく
Web アプリ/APILambda + API Gateway または ECS Fargate
定常稼働のサービスECS Fargate(インスタンス管理が要らない)
Auto Scaling したいバッチaws_launch_template + aws_autoscaling_group
SSH せずに作業AWS Cloud9 / SSM Session Manager

「サーバの OS を自由にいじりたい」「特殊なソフトウェアを動かす」場合だけ EC2、それ以外は Lambda か ECS Fargate を第一候補 にするのが 2026 年のセオリー。