★ 初級

04. S3 とストレージ

S3 はオブジェクトストレージ。中身は「ファイル + メタデータ」の集合体です。Terraform の S3 リソースは 2022 年以降「分離型」に進化しており、書き方が以前と大きく変わっているので注意。

バケット ─ 何が変わったか

かつての aws_s3_bucket は「バージョニング・暗号化・公開設定・ポリシー・ロギング・通知」など、何でもかんでも 1 ブロックの中に書く設計でした。AWS Provider 5.x からは 各設定が独立したリソース に切り出されています。

分離型のメリット

aws_s3_bucket(最小)

resource "aws_s3_bucket" "data" {
  bucket = "my-app-data-20260510"   # 全世界で一意な名前
}

これだけ。本当にバケットを作るだけ。中身の動作(暗号化、バージョニング、公開可否)は別リソースで設定します。

推奨セット(暗号化+公開遮断+バージョニング)

2026 年現在、S3 を作ったら必ずこの 3 つは付ける のが既定のセキュリティ姿勢です。

resource "aws_s3_bucket" "data" {
  bucket = "my-app-data-20260510"
}

# 1. パブリックアクセスを完全遮断
resource "aws_s3_bucket_public_access_block" "data" {
  bucket = aws_s3_bucket.data.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

# 2. サーバーサイド暗号化(SSE-S3 = AWS 管理キー)
resource "aws_s3_bucket_server_side_encryption_configuration" "data" {
  bucket = aws_s3_bucket.data.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}

# 3. バージョニング(誤削除からの保護)
resource "aws_s3_bucket_versioning" "data" {
  bucket = aws_s3_bucket.data.id

  versioning_configuration {
    status = "Enabled"
  }
}

2024 年以降、AWS は 新規バケットはデフォルトで暗号化+ public 遮断 ですが、明示的に書いておく のが正攻法。設定が変わっても Terraform の state 上で管理されているので安心。

バケットポリシー

ポリシーは「このバケットに誰がアクセスできるか」のルール。aws_s3_bucket_policy リソースとして書きます。文字列の JSON より、data "aws_iam_policy_document" で組み立てるほうが読みやすい(05 章)。

data "aws_iam_policy_document" "data" {
  statement {
    sid     = "AllowCloudFrontOAC"
    effect  = "Allow"
    actions = ["s3:GetObject"]

    principals {
      type        = "Service"
      identifiers = ["cloudfront.amazonaws.com"]
    }

    resources = ["${aws_s3_bucket.data.arn}/*"]

    condition {
      test     = "StringEquals"
      variable = "AWS:SourceArn"
      values   = [aws_cloudfront_distribution.site.arn]
    }
  }
}

resource "aws_s3_bucket_policy" "data" {
  bucket = aws_s3_bucket.data.id
  policy = data.aws_iam_policy_document.data.json
}

ライフサイクル(古いオブジェクトの自動削除)

ログを長く貯めると S3 もタダではないので、自動で消すかストレージクラスを下げます。

resource "aws_s3_bucket_lifecycle_configuration" "logs" {
  bucket = aws_s3_bucket.logs.id

  rule {
    id     = "expire-old-logs"
    status = "Enabled"

    filter { prefix = "access/" }

    transition {
      days          = 30
      storage_class = "STANDARD_IA"   # 30 日後 IA へ
    }
    transition {
      days          = 90
      storage_class = "GLACIER"        # 90 日後 Glacier へ
    }
    expiration {
      days = 365                        # 365 日後に削除
    }

    noncurrent_version_expiration {
      noncurrent_days = 30              # 古いバージョンは 30 日で削除
    }
  }
}

aws_s3_object でファイルを置く

Terraform から S3 にファイルをアップロードできます。fileset() でディレクトリ全体を一括アップロードするパターンが定番。

locals {
  content_types = {
    "html" = "text/html; charset=utf-8"
    "css"  = "text/css"
    "js"   = "application/javascript"
    "png"  = "image/png"
    "svg"  = "image/svg+xml"
    "json" = "application/json"
    "txt"  = "text/plain"
  }
}

resource "aws_s3_object" "site_files" {
  for_each = fileset("${path.module}/../public", "**/*")

  bucket = aws_s3_bucket.site.id
  key    = each.value
  source = "${path.module}/../public/${each.value}"

  # ファイル変更を検知して再アップロード
  etag = filemd5("${path.module}/../public/${each.value}")

  content_type = lookup(
    local.content_types,
    reverse(split(".", each.value))[0],
    "application/octet-stream"
  )
}
このサイトもこの形 hcl-guide.com の HTML / CSS / JS は、まさにこのパターンで Terraform から S3 にデプロイされています。08 章 で全体構成を見ます。

静的ウェブホスティング

S3 単独で公開もできますが、HTTPS が使えないCDN がない ので、独自ドメインの本番運用には CloudFront 経由が標準です。

resource "aws_s3_bucket_website_configuration" "site" {
  bucket = aws_s3_bucket.site.id

  index_document { suffix = "index.html" }
  error_document { key    = "404.html" }
}

推奨: バケットはプライベート(public_access_block 全 ON)のまま、CloudFront + OAC 経由で公開。SPA や静的サイトでも HTTPS は必須。