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:
- Custom Domains (recommended) - Use the
custom_domainsfield to bind a domain directly to your bucket. Configurecors_rulesto control which origins can access the content. - Cloudflare Workers - Proxy requests through a Worker for more complex logic
- 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¶
- Naming Convention: Use descriptive names like
projectname-purpose-env - Location Strategy: Choose locations based on user proximity and compliance requirements
- Jurisdiction: Always set jurisdiction based on legal and compliance requirements (GDPR, data sovereignty)
- Storage Class: Use
Standardfor frequently accessed data andInfrequentAccessfor archives/backups - Separation: Keep different types of data in separate buckets
- Access Control: Use R2 API tokens with minimal required permissions
- Monitoring: Track bucket usage in Cloudflare dashboard
Prerequisites¶
- Cloudflare account with R2 enabled
- Account ID from Cloudflare dashboard
- 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.
Related¶
- Pages Module - Deploy static sites that use R2 storage
- DNS Module - Configure custom domains for R2 buckets
- Cloudflare R2 Documentation
- R2 API Reference