★ 初級

05. Cloud Storage

GCP のオブジェクトストレージ。AWS S3 相当ですが、命名規約・storage class・IAM 権限 に独特な点があります。uniform_bucket_level_access 必須が今のベストプラクティス。

バケットの基本

resource "google_storage_bucket" "data" {
  name     = "myapp-prd-data-${random_id.suffix.hex}"   # 全世界一意
  location = "ASIA-NORTHEAST1"   # リージョン or マルチリージョン (例: "ASIA")

  storage_class               = "STANDARD"
  uniform_bucket_level_access = true   # ACL を無効化、IAM のみで制御(推奨)

  force_destroy = false   # 中身があると destroy 拒否

  labels = local.common_labels
}
命名の制約 バケット名は全世界一意、3-63 文字、英小文字/数字/ハイフン/ピリオド/アンダースコア。googgoogle は使えない。一度付けると変更不可。

Storage Class

class用途最低保管取得料金
STANDARD頻繁アクセス(汎用)なし無料
NEARLINE月 1 回程度30 日あり
COLDLINE四半期 1 回90 日
ARCHIVE年 1 回365 日最大

S3 と違って 同一バケット内で複数 class を持てる。ライフサイクルで自動 transition すれば、ホット→コールド の自動降格が可能。

セキュリティ推奨設定

resource "google_storage_bucket" "data" {
  # ... 基本項目 ...

  # パブリックアクセスを完全遮断
  public_access_prevention = "enforced"

  # ACL 廃止、IAM のみに統一
  uniform_bucket_level_access = true

  # バージョニング
  versioning {
    enabled = true
  }

  # CMEK 暗号化(次章で KMS キーを作る)
  encryption {
    default_kms_key_name = google_kms_crypto_key.bucket.id
  }

  # ログ
  logging {
    log_bucket        = google_storage_bucket.logs.name
    log_object_prefix = "data/"
  }

  # CORS(Web アプリから直接アクセスする時)
  cors {
    origin          = ["https://myapp.com"]
    method          = ["GET", "HEAD"]
    response_header = ["*"]
    max_age_seconds = 3600
  }
}

ライフサイクル

resource "google_storage_bucket" "data" {
  # ...
  lifecycle_rule {
    condition {
      age = 30   # 30 日経過
    }
    action {
      type          = "SetStorageClass"
      storage_class = "NEARLINE"
    }
  }

  lifecycle_rule {
    condition {
      age = 365
    }
    action {
      type = "Delete"   # 1 年経ったら削除
    }
  }

  lifecycle_rule {
    condition {
      with_state         = "ARCHIVED"   # 古いバージョン
      num_newer_versions = 3            # 3 世代より古いもの
    }
    action {
      type = "Delete"
    }
  }
}

IAM

GCP IAM の 3 リソースの違い(06 章参照)が Cloud Storage でも同様に重要。

# ★ 推奨: member(個別追加、最も安全)
resource "google_storage_bucket_iam_member" "vm_read" {
  bucket = google_storage_bucket.data.name
  role   = "roles/storage.objectViewer"
  member = "serviceAccount:${google_service_account.vm.email}"
}

# binding(ロールごとに複数 member を一括管理。他で追加した member は消える)
resource "google_storage_bucket_iam_binding" "viewers" {
  bucket = google_storage_bucket.data.name
  role   = "roles/storage.objectViewer"
  members = [
    "serviceAccount:${google_service_account.vm.email}",
    "group:viewers@example.com",
  ]
}

# policy は使わない(バケット全 IAM を上書きする超危険な操作)

よく使うロール

ロール権限
roles/storage.objectViewerオブジェクト読み取り
roles/storage.objectCreator書き込みのみ(読まない)
roles/storage.objectAdmin読み書き削除
roles/storage.adminバケット管理含む全権

オブジェクトアップロード

resource "google_storage_bucket_object" "logo" {
  name   = "images/logo.png"
  bucket = google_storage_bucket.data.name
  source = "${path.module}/assets/logo.png"

  content_type = "image/png"

  metadata = {
    uploaded_by = "terraform"
  }
}

# ディレクトリ一括(fileset で)
resource "google_storage_bucket_object" "site_files" {
  for_each = fileset("${path.module}/../public", "**/*")

  name   = each.value
  bucket = google_storage_bucket.site.name
  source = "${path.module}/../public/${each.value}"

  content_type = lookup({
    "html" = "text/html"
    "css"  = "text/css"
    "js"   = "application/javascript"
    "png"  = "image/png"
  }, lower(reverse(split(".", each.value))[0]), "application/octet-stream")
}

静的サイトホスティング

resource "google_storage_bucket" "site" {
  name     = "site.myapp.com"
  location = "ASIA"

  website {
    main_page_suffix = "index.html"
    not_found_page   = "404.html"
  }

  uniform_bucket_level_access = true
}

# 全ユーザーに read 権限(公開)
resource "google_storage_bucket_iam_member" "public" {
  bucket = google_storage_bucket.site.name
  role   = "roles/storage.objectViewer"
  member = "allUsers"
}

独自ドメイン + HTTPS で公開するなら Cloud Load Balancer を前段に置きます(10 章)。