04. S3 とストレージ
S3 はオブジェクトストレージ。中身は「ファイル + メタデータ」の集合体です。Terraform の S3 リソースは 2022 年以降「分離型」に進化しており、書き方が以前と大きく変わっているので注意。
バケット ─ 何が変わったか
かつての aws_s3_bucket は「バージョニング・暗号化・公開設定・ポリシー・ロギング・通知」など、何でもかんでも 1 ブロックの中に書く設計でした。AWS Provider 5.x からは 各設定が独立したリソース に切り出されています。
分離型のメリット
- 差分が小さくなる(バージョニングだけ変えたい時、バケット全体を再評価しなくて済む)
- 権限を分けやすい(暗号化設定はセキュリティチーム、ポリシーはサービスチーム、など)
- 1 つのバケットに対して、設定リソースを段階的に足せる
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"
)
}
静的ウェブホスティング
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 は必須。