Skip to content

R2 Module

The R2 module provides a simple way to manage Cloudflare R2 storage buckets. R2 is Cloudflare's object storage service that is S3-compatible with zero egress fees.

Provider Version

This module requires Cloudflare Terraform Provider v5.0 or higher for full support of jurisdiction and storage_class arguments.

Features

  • Multiple Buckets: Create and manage multiple R2 buckets
  • Location Control: Choose specific regions or use auto location
  • CORS Configuration: Define cross-origin resource sharing rules per bucket
  • Custom Domains: Bind custom domains to buckets for public access
  • S3 Compatible: Works with S3-compatible tools and SDKs
  • Zero Egress Fees: No charges for data transfer out
  • Global Performance: Cloudflare's global network for fast access

Basic Usage

module "r2" {
  source = "AutomationDojo/management/cloudflare//modules/r2"
  version = "2.3.0"

  account_id = var.cloudflare_account_id

  buckets = [
    {
      name          = "my-storage-bucket"
      location      = "eeur"
      jurisdiction  = "eu"
      storage_class = "Standard"
    }
  ]
}

Examples

Single Bucket with EU Location

Create a bucket in the European region with EU jurisdiction:

module "r2_storage" {
  source     = "AutomationDojo/management/cloudflare//modules/r2"
  version = "2.3.0"
  account_id = var.cloudflare_account_id

  buckets = [
    {
      name          = "app-storage"
      location      = "eeur"
      jurisdiction  = "eu"
      storage_class = "Standard"
    }
  ]
}

Multiple Buckets with Specific Locations

Create buckets in different regions for compliance or performance:

module "r2_multi_region" {
  source     = "AutomationDojo/management/cloudflare//modules/r2"
  version = "2.3.0"
  account_id = var.cloudflare_account_id

  buckets = [
    {
      name          = "eu-user-data"
      location      = "weur"
      jurisdiction  = "eu"
      storage_class = "Standard"
    },
    {
      name          = "us-user-data"
      location      = "wnam"
      jurisdiction  = "us"
      storage_class = "Standard"
    },
    {
      name          = "asia-user-data"
      location      = "apac"
      jurisdiction  = "apac"
      storage_class = "Standard"
    }
  ]
}

Application Storage Setup

Complete setup for different application needs:

module "app_storage" {
  source     = "AutomationDojo/management/cloudflare//modules/r2"
  version = "2.3.0"
  account_id = var.cloudflare_account_id

  buckets = [
    {
      name          = "static-assets"
      location      = "eeur"
      jurisdiction  = "eu"
      storage_class = "Standard"
    },
    {
      name          = "user-uploads"
      location      = "eeur"
      jurisdiction  = "eu"
      storage_class = "Standard"
    },
    {
      name          = "backups"
      location      = "weur"
      jurisdiction  = "eu"
      storage_class = "InfrequentAccess"
    },
    {
      name          = "logs"
      location      = "eeur"
      jurisdiction  = "eu"
      storage_class = "InfrequentAccess"
    }
  ]
}

Multi-Environment Setup

Separate buckets for different environments:

module "r2_production" {
  source     = "AutomationDojo/management/cloudflare//modules/r2"
  version = "2.3.0"
  account_id = var.cloudflare_account_id

  buckets = [
    {
      name          = "prod-assets"
      location      = "eeur"
      jurisdiction  = "eu"
      storage_class = "Standard"
    },
    {
      name          = "prod-data"
      location      = "weur"
      jurisdiction  = "eu"
      storage_class = "Standard"
    }
  ]
}

module "r2_staging" {
  source     = "AutomationDojo/management/cloudflare//modules/r2"
  version = "2.3.0"
  account_id = var.cloudflare_account_id

  buckets = [
    {
      name          = "staging-assets"
      location      = "eeur"
      jurisdiction  = "eu"
      storage_class = "Standard"
    },
    {
      name          = "staging-data"
      location      = "eeur"
      jurisdiction  = "eu"
      storage_class = "Standard"
    }
  ]
}

Bucket with CORS Rules

Allow specific origins to access your bucket content:

module "r2_media" {
  source     = "AutomationDojo/management/cloudflare//modules/r2"
  version = "2.3.0"
  account_id = var.cloudflare_account_id

  buckets = [
    {
      name     = "media-assets"
      location = "eeur"

      cors_rules = [
        {
          allowed_methods = ["GET"]
          allowed_origins = ["https://example.com", "https://www.example.com"]
        },
        {
          allowed_methods = ["GET", "PUT", "POST"]
          allowed_origins = ["https://admin.example.com"]
          allowed_headers = ["Content-Type", "Authorization"]
          expose_headers  = ["ETag"]
          max_age_seconds = 3600
        }
      ]
    }
  ]
}

Bucket with Custom Domain

Serve bucket content from a custom domain:

module "r2_cdn" {
  source     = "AutomationDojo/management/cloudflare//modules/r2"
  version = "2.3.0"
  account_id = var.cloudflare_account_id

  buckets = [
    {
      name     = "cdn-assets"
      location = "eeur"

      custom_domains = [
        {
          domain  = "media.example.com"
          zone_id = var.cloudflare_zone_id
        }
      ]
    }
  ]
}

Bucket with CORS and Custom Domain

Combine CORS rules with a custom domain for a complete public media setup:

module "r2_public" {
  source     = "AutomationDojo/management/cloudflare//modules/r2"
  version = "2.3.0"
  account_id = var.cloudflare_account_id

  buckets = [
    {
      name     = "public-media"
      location = "eeur"

      cors_rules = [
        {
          allowed_methods = ["GET"]
          allowed_origins = ["https://example.com"]
        }
      ]

      custom_domains = [
        {
          domain  = "media.example.com"
          zone_id = var.cloudflare_zone_id
          enabled = true
          min_tls = "1.2"
        }
      ]
    }
  ]
}

Integration with Pages

Use R2 buckets with Cloudflare Pages for static assets:

module "r2_storage" {
  source     = "AutomationDojo/management/cloudflare//modules/r2"
  version = "2.3.0"
  account_id = var.cloudflare_account_id

  buckets = [
    {
      name          = "website-images"
      location      = "eeur"
      jurisdiction  = "eu"
      storage_class = "Standard"
    },
    {
      name          = "website-videos"
      location      = "eeur"
      jurisdiction  = "eu"
      storage_class = "Standard"
    }
  ]
}

module "pages_site" {
  source     = "AutomationDojo/management/cloudflare//modules/pages"
  version = "2.3.0"
  account_id = var.cloudflare_account_id

  projects = [
    {
      name              = "my-website"
      production_branch = "main"
    }
  ]
}

Available Locations

Location Code Region Description
wnam Western North America US West Coast
enam Eastern North America US East Coast
weur Western Europe EU West
eeur Eastern Europe EU East (default)
apac Asia-Pacific Asia Pacific region

Location Selection

The default location is eeur (Eastern Europe). Choose a location close to your users or based on compliance requirements.

Jurisdiction Options

Jurisdiction determines the legal framework and compliance requirements for your data:

Jurisdiction Description Use Cases
eu European Union (default) GDPR compliance, EU data residency
us United States US-based applications, US compliance
apac Asia-Pacific APAC region applications

Compliance

Choose jurisdiction based on your data residency and compliance requirements (GDPR, data sovereignty, etc.).

Storage Classes

R2 offers different storage classes optimized for different access patterns:

Storage Class Description Use Cases Cost
Standard Frequent access (default) Active data, CDN content, frequently accessed files Higher storage cost, lower access cost
InfrequentAccess Infrequent access Backups, archives, logs, old data Lower storage cost, higher access cost

Cost Optimization

Use Standard for frequently accessed data and InfrequentAccess for backups and archives to optimize costs.

Inputs

Name Description Type Required
account_id Cloudflare account ID string Yes
buckets List of R2 buckets to create list(object) No (default: [])

Bucket Object

Field Description Type Required Default
name Bucket name (must be globally unique) string Yes -
location Bucket location/region string No eeur
jurisdiction Data jurisdiction (legal framework) string No eu
storage_class Storage class for cost optimization string No Standard
cors_rules List of CORS rules for the bucket list(object) No []
custom_domains List of custom domains to bind to the bucket list(object) No []

CORS Rule Object

Field Description Type Required Default
id Optional identifier for the rule string No null
allowed_methods HTTP methods to allow (e.g. GET, PUT, POST) list(string) Yes -
allowed_origins Origins allowed to make requests (e.g. https://example.com) list(string) Yes -
allowed_headers Headers allowed in preflight requests list(string) No null
expose_headers Headers exposed to the browser list(string) No null
max_age_seconds How long the preflight response is cached (seconds) number No null

Custom Domain Object

Field Description Type Required Default
domain The custom domain to bind (e.g. media.example.com) string Yes -
zone_id Cloudflare zone ID that owns the domain string Yes -
enabled Whether the custom domain is enabled bool No true
min_tls Minimum TLS version (e.g. 1.2) string No null

Outputs

Name Description
buckets Map of all created R2 buckets with their details
bucket_cors Map of R2 bucket CORS configurations
custom_domains Map of R2 custom domain bindings

Example Output Usage

output "r2_bucket_names" {
  description = "Names of created R2 buckets"
  value       = [for bucket in module.r2.buckets : bucket.name]
}

output "r2_bucket_ids" {
  description = "IDs of created R2 buckets"
  value       = { for k, v in module.r2.buckets : k => v.id }
}

Using R2 Buckets

S3-Compatible Access

R2 buckets are S3-compatible. You can use AWS SDKs and tools:

# Configure AWS CLI for R2
aws configure --profile r2
# AWS Access Key ID: Your R2 Access Key
# AWS Secret Access Key: Your R2 Secret Key
# Default region: auto
# Default output format: json

# Upload file
aws s3 cp file.jpg s3://my-storage-bucket/ --profile r2 \
  --endpoint-url https://<account-id>.r2.cloudflarestorage.com

# List files
aws s3 ls s3://my-storage-bucket/ --profile r2 \
  --endpoint-url https://<account-id>.r2.cloudflarestorage.com

Accessing via API

// Using @aws-sdk/client-s3
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";

const S3 = new S3Client({
  region: "auto",
  endpoint: "https://<account-id>.r2.cloudflarestorage.com",
  credentials: {
    accessKeyId: "your-access-key",
    secretAccessKey: "your-secret-key",
  },
});

await S3.send(
  new PutObjectCommand({
    Bucket: "my-storage-bucket",
    Key: "file.jpg",
    Body: fileData,
  })
);

Public Access

To serve files publicly, you can:

  1. Custom Domains (recommended) - Use the custom_domains field to bind a domain directly to your bucket. Configure cors_rules to control which origins can access the content.
  2. Cloudflare Workers - Proxy requests through a Worker for more complex logic
  3. Presigned URLs - Generate temporary access URLs for private content

Important Notes

Bucket Names

Bucket names must be globally unique across all Cloudflare accounts. Choose descriptive, unique names.

Access Keys

You'll need to create R2 API tokens in the Cloudflare dashboard to access your buckets programmatically.

Cost Optimization

R2 has no egress fees, making it ideal for frequently accessed data and CDN use cases.

Use Cases

Static Asset Storage

Store images, videos, and other static files for your website:

buckets = [
  {
    name          = "cdn-assets"
    location      = "eeur"
    jurisdiction  = "eu"
    storage_class = "Standard"
  }
]

Data Lake

Store large datasets for analytics:

buckets = [
  {
    name          = "analytics-data-lake"
    location      = "weur"
    jurisdiction  = "eu"  # EU for GDPR compliance
    storage_class = "InfrequentAccess"  # Analytics data accessed less frequently
  }
]

Backup Storage

Store application backups:

buckets = [
  {
    name          = "database-backups"
    location      = "wnam"
    jurisdiction  = "us"
    storage_class = "InfrequentAccess"  # Backups accessed rarely
  }
]

User Uploads

Handle user-generated content:

buckets = [
  {
    name          = "user-profiles"
    location      = "eeur"
    jurisdiction  = "eu"
    storage_class = "Standard"  # Frequently accessed profile data
  },
  {
    name          = "user-content"
    location      = "eeur"
    jurisdiction  = "eu"
    storage_class = "Standard"  # Active user content
  }
]

Best Practices

  1. Naming Convention: Use descriptive names like projectname-purpose-env
  2. Location Strategy: Choose locations based on user proximity and compliance requirements
  3. Jurisdiction: Always set jurisdiction based on legal and compliance requirements (GDPR, data sovereignty)
  4. Storage Class: Use Standard for frequently accessed data and InfrequentAccess for archives/backups
  5. Separation: Keep different types of data in separate buckets
  6. Access Control: Use R2 API tokens with minimal required permissions
  7. Monitoring: Track bucket usage in Cloudflare dashboard

Prerequisites

  1. Cloudflare account with R2 enabled
  2. Account ID from Cloudflare dashboard
  3. R2 API tokens for programmatic access (created separately)

Troubleshooting

Bucket Name Already Exists

Bucket names are globally unique. Try a different name or add a unique prefix/suffix.

Access Denied

Ensure your R2 API token has the correct permissions for the operations you're performing.

Location Not Available

Check that the location code is valid. Use auto if unsure.