Skip to main content

Provision the Upload Archive with Terraform (Google Cloud Storage)

This page is the infrastructure-as-code alternative to Part 2 of the

Configure File Upload Capture (Google Cloud Storage)

guide. It produces the same resources as the console walkthrough there — a hardened Cloud Storage bucket, an optional Cloud KMS customer-managed key, and the bucket-scoped IAM grant the Anzenna writer service account uses — in a single terraform apply.

Before starting:

When the apply finishes, return to the Google Cloud Storage guide and continue at Part 3 (Register everything in Anzenna).

terraform {
required_version = ">= 1.5"

required_providers {
google = { source = "hashicorp/google", version = "~> 6.0" }
random = { source = "hashicorp/random", version = "~> 3.6" }
}
}

variable "project_id" {
type = string
description = "The GCP project that will own the archive bucket."
}

variable "bucket_location" {
type = string
default = "us-west1"
description = "Bucket location. Must also be a valid Cloud KMS location, since the CMEK key ring is created in the same location. For a multi-region bucket (e.g. US) use the matching KMS multi-region (e.g. us)."
}

variable "customer_label" {
type = string
description = "Short identifier. Becomes part of the bucket and key names. Lowercase alphanumeric or hyphens, 2-32 chars."

validation {
condition = can(regex("^[a-z0-9][a-z0-9-]{0,30}[a-z0-9]$", var.customer_label))
error_message = "customer_label must be lowercase alphanumeric or hyphens, 2-32 chars, not starting/ending with a hyphen."
}
}

variable "anzenna_writer_sa_email" {
type = string
default = "user-content@anzenna-live.iam.gserviceaccount.com"
description = "The Anzenna writer service account you grant write access to. Default is Anzenna's production (live) signer. Override per env with the value Anzenna support gives you."
}

variable "writer_role" {
type = string
default = "roles/storage.objectCreator"
description = "Role granted to the Anzenna writer SA, scoped to the bucket. objectCreator is create-only (cannot list, read, overwrite, or delete), which fits archival's write-once-with-unique-keys model. Switch to roles/storage.objectUser only if Anzenna must overwrite a stable path."
}

variable "object_expiration_days" {
type = number
default = 90
description = "Lifecycle expiry for live object versions."
}

variable "enable_cmek" {
type = bool
default = true
description = "Encrypt the bucket with a customer-managed Cloud KMS key. Set false for Google-managed encryption (still encrypted at rest, just not with your own key)."
}

provider "google" {
project = var.project_id
region = var.bucket_location
}

resource "random_id" "bucket_suffix" {
byte_length = 4
}

locals {
name = "anzenna-upload-${var.customer_label}"
bucket_name = "${local.name}-${random_id.bucket_suffix.hex}"
}

# Customer-managed key for encryption at rest. Key ring and key live in your
# project, in the same location as the bucket. 90-day rotation; never exported.
resource "google_kms_key_ring" "upload" {
count = var.enable_cmek ? 1 : 0
name = "${local.name}-keyring"
location = var.bucket_location
}

resource "google_kms_crypto_key" "upload" {
count = var.enable_cmek ? 1 : 0
name = "${local.name}-key"
key_ring = google_kms_key_ring.upload[0].id
rotation_period = "7776000s" # 90 days

# Destroying a CMEK key makes every object it encrypted permanently
# unrecoverable — guard it. Flip to false only for a disposable test deploy.
lifecycle {
prevent_destroy = true
}
}

# The GCS service agent (not the writer SA) encrypts/decrypts objects at rest,
# so it — and only it — needs cryptoKeyEncrypterDecrypter on the key. The bucket
# depends on this grant existing first, or create fails.
data "google_storage_project_service_account" "gcs" {
count = var.enable_cmek ? 1 : 0
}

resource "google_kms_crypto_key_iam_member" "gcs_agent" {
count = var.enable_cmek ? 1 : 0
crypto_key_id = google_kms_crypto_key.upload[0].id
role = "roles/cloudkms.cryptoKeyEncrypterDecrypter"
member = "serviceAccount:${data.google_storage_project_service_account.gcs[0].email_address}"
}

# Upload bucket: uniform bucket-level access + public-access-prevention enforced
# (no ACLs, no public exposure possible), versioning on, lifecycle expiry.
resource "google_storage_bucket" "upload" {
name = local.bucket_name
location = var.bucket_location
storage_class = "STANDARD"
force_destroy = false
uniform_bucket_level_access = true
public_access_prevention = "enforced"

versioning {
enabled = true
}

dynamic "encryption" {
for_each = var.enable_cmek ? [1] : []
content {
default_kms_key_name = google_kms_crypto_key.upload[0].id
}
}

# Expire live objects after your retention window, and reap noncurrent
# versions 7 days after they're superseded.
lifecycle_rule {
action {
type = "Delete"
}
condition {
age = var.object_expiration_days
with_state = "LIVE"
}
}

lifecycle_rule {
action {
type = "Delete"
}
condition {
days_since_noncurrent_time = 7
with_state = "ARCHIVED"
}
}

depends_on = [google_kms_crypto_key_iam_member.gcs_agent]
}

# The single grant the whole onboarding hinges on: the Anzenna writer SA gets a
# write-only role, scoped to this bucket only — not the project. No read, list,
# or delete.
resource "google_storage_bucket_iam_member" "writer" {
bucket = google_storage_bucket.upload.name
role = var.writer_role
member = "serviceAccount:${var.anzenna_writer_sa_email}"
}

output "bucket_name" {
value = google_storage_bucket.upload.name
}
  1. Save the file (e.g. main.tf) and create a terraform.tfvars with your project and label:

    project_id = "<your-project-id>" customer_label = "yourorg"

    If your tenant is not on production, add anzenna_writer_sa_email with the value Anzenna support gave you.

  2. Authenticate:

    gcloud auth application-default login
  3. Run terraform init && terraform apply.

  4. Read the output you'll paste into Anzenna in Part 3:

    terraform output bucket_name

    The destination form maps as: Bucket name = bucket_name. Leave Object key prefix blank.

Domain-restricted sharing

If your organization enforces the iam.allowedPolicyMemberDomains org policy, the writer grant will fail until you add Anzenna's Google Workspace customer ID (C03htzkal) to the allowed list — see 2b in the Google Cloud Storage guide. Do that first, then re-run terraform apply.

You're done with Google Cloud. Return to the

Google Cloud Storage setup guide

and continue at Part 3 (Register everything in Anzenna).


Need help? Contact Anzenna Support at support@anzenna.ai for assistance.