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:
Complete Part 1 in the
Google Cloud Storage guide
(generate an age recipient keypair).
Get your environment's Anzenna writer service account email from Anzenna support. For production (
app.anzenna.ai) it isuser-content@anzenna-live.iam.gserviceaccount.com— the default in the module below.
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
}
Save the file (e.g.
main.tf) and create aterraform.tfvarswith your project and label:project_id = "<your-project-id>" customer_label = "yourorg"
If your tenant is not on production, add
anzenna_writer_sa_emailwith the value Anzenna support gave you.Authenticate:
gcloud auth application-default loginRun
terraform init && terraform apply.Read the output you'll paste into Anzenna in Part 3:
terraform output bucket_nameThe destination form maps as: Bucket name =
bucket_name. Leave Object key prefix blank.
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.