From daa896f580d330b9f36e9dc504b58547daedb571 Mon Sep 17 00:00:00 2001 From: Donn Felker Date: Fri, 1 May 2026 11:45:05 -0400 Subject: [PATCH 1/3] feat(terraform): allow overriding R2 media bucket name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an optional r2_media_bucket_name variable so the bucket can be pre-created out-of-band and the deployment can point at it instead of having Terraform create one. This unblocks environments where the Cloudflare API token used by Terraform/CI does not have R2 bucket- creation rights and an admin must provision the bucket manually. When the override is empty, behavior is unchanged: the bucket is created as open-inspect-media-. When set, that exact name is used and operators are expected to terraform import the existing bucket so applies do not try to recreate it. The example tfvars documents manual-setup requirements (same account, private, no CORS/lifecycle, matching location) and clarifies that the Terraform token only needs Workers R2 Storage Read plus Workers Scripts Edit once the bucket exists — object-level R2 permissions are never required because runtime access flows through the in-account MEDIA_BUCKET Worker binding. Co-Authored-By: Claude Opus 4.7 (1M context) --- terraform/environments/production/r2.tf | 2 +- .../production/terraform.tfvars.example | 30 +++++++++++++++++++ .../environments/production/variables.tf | 6 ++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/terraform/environments/production/r2.tf b/terraform/environments/production/r2.tf index beb3684c1..d7421a2c7 100644 --- a/terraform/environments/production/r2.tf +++ b/terraform/environments/production/r2.tf @@ -4,6 +4,6 @@ resource "cloudflare_r2_bucket" "media" { account_id = var.cloudflare_account_id - name = "open-inspect-media-${local.name_suffix}" + name = var.r2_media_bucket_name != "" ? var.r2_media_bucket_name : "open-inspect-media-${local.name_suffix}" location = var.r2_media_location } diff --git a/terraform/environments/production/terraform.tfvars.example b/terraform/environments/production/terraform.tfvars.example index b0a29cdf1..258ce0f37 100644 --- a/terraform/environments/production/terraform.tfvars.example +++ b/terraform/environments/production/terraform.tfvars.example @@ -174,6 +174,36 @@ deployment_name = "" # Path to project root (relative to this directory) project_root = "../../../" +# Override the R2 media bucket name (optional) +# Defaults to "open-inspect-media-" when left empty. +# Set this when the bucket must be pre-created out-of-band — for example, when +# the Cloudflare API token used by Terraform doesn't have permission to create +# R2 buckets and an admin had to provision one with a specific name. +# +# Manual setup requirements (when an admin pre-creates the bucket): +# - Must live in the same Cloudflare account as the Workers (cloudflare_account_id). +# - Private bucket — do NOT enable public access. The control-plane Worker +# reads/writes objects through its binding; clients never hit R2 directly. +# - No CORS rules or lifecycle policies are required. +# - Location should match r2_media_location (default ENAM). +# +# Create with wrangler (equivalent to what Terraform would do): +# wrangler r2 bucket create --location ENAM +# Or create via the Cloudflare dashboard: R2 -> Create bucket (Standard storage class). +# +# After manual creation, import the bucket so Terraform manages the binding without +# trying to recreate it: +# terraform import cloudflare_r2_bucket.media / +# +# Worker runtime needs (granted automatically through the binding, listed for reference): +# - R2 object-level: Read, Write, Delete on this bucket only. +# The Terraform/Cloudflare API token does NOT need object-level R2 permissions — +# it only manages the bucket resource and the Worker binding. Required token scopes: +# - Workers R2 Storage: Edit (only needed when Terraform creates/manages the bucket; +# if the bucket is pre-created and imported, drop to Read or omit) +# - Workers Scripts: Edit (to attach the MEDIA_BUCKET binding to the Worker) +# r2_media_bucket_name = "" + # ============================================================================= # Initial Deployment Flags # ============================================================================= diff --git a/terraform/environments/production/variables.tf b/terraform/environments/production/variables.tf index 599741bc2..56e52e782 100644 --- a/terraform/environments/production/variables.tf +++ b/terraform/environments/production/variables.tf @@ -377,6 +377,12 @@ variable "r2_media_location" { default = "ENAM" } +variable "r2_media_bucket_name" { + description = "Override the R2 media bucket name. Leave empty to use the default 'open-inspect-media-'. Set this when the bucket must be pre-created out-of-band (e.g. when the Terraform credentials cannot create R2 buckets)." + type = string + default = "" +} + # ============================================================================= # Access Control # ============================================================================= From 205532b829801932ff69ff852138bcbcc18b1762 Mon Sep 17 00:00:00 2001 From: Cole Murray Date: Thu, 7 May 2026 23:30:52 -0700 Subject: [PATCH 2/3] docs(terraform): add /default jurisdiction to R2 bucket import example The Cloudflare provider expects an import ID of //. Omitting the jurisdiction on a non-jurisdictional bucket leaves Terraform trying to add jurisdiction = "default" on every subsequent plan/apply. --- terraform/environments/production/terraform.tfvars.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terraform/environments/production/terraform.tfvars.example b/terraform/environments/production/terraform.tfvars.example index df48e7205..17e044ffa 100644 --- a/terraform/environments/production/terraform.tfvars.example +++ b/terraform/environments/production/terraform.tfvars.example @@ -235,7 +235,7 @@ project_root = "../../../" # # After manual creation, import the bucket so Terraform manages the binding without # trying to recreate it: -# terraform import cloudflare_r2_bucket.media / +# terraform import cloudflare_r2_bucket.media //default # # Worker runtime needs (granted automatically through the binding, listed for reference): # - R2 object-level: Read, Write, Delete on this bucket only. From 7a920ec8e52955ab420a1c20ee8ba17870262460 Mon Sep 17 00:00:00 2001 From: Cole Murray Date: Thu, 7 May 2026 23:34:13 -0700 Subject: [PATCH 3/3] docs(terraform): warn about object loss when renaming R2 bucket without import Operators upgrading an existing deployment to the new r2_media_bucket_name override would otherwise see Terraform plan a destroy-then-create on the default-named bucket, deleting all stored objects. --- terraform/environments/production/terraform.tfvars.example | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/terraform/environments/production/terraform.tfvars.example b/terraform/environments/production/terraform.tfvars.example index 17e044ffa..ed29f3aaf 100644 --- a/terraform/environments/production/terraform.tfvars.example +++ b/terraform/environments/production/terraform.tfvars.example @@ -237,6 +237,13 @@ project_root = "../../../" # trying to recreate it: # terraform import cloudflare_r2_bucket.media //default # +# WARNING — renaming an existing Terraform-managed bucket: +# If you previously deployed without this variable (Terraform owns the default-named +# bucket) and now set r2_media_bucket_name to a different name WITHOUT first running +# `terraform import` on the new name, Terraform will destroy the existing bucket and +# create a new one — permanently deleting all stored objects. Migrate objects first +# (e.g. with rclone) and import the new bucket before applying. +# # Worker runtime needs (granted automatically through the binding, listed for reference): # - R2 object-level: Read, Write, Delete on this bucket only. # The Terraform/Cloudflare API token does NOT need object-level R2 permissions —