Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions tofu/remote-state/cf/DOCS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
## Overview

This OpenTofu module is responsible for provisioning the foundational infrastructure for managing OpenTofu state within Cloudflare R2. It automates the creation of the R2 bucket and generates the necessary S3-compatible access credentials for remote state storage.

## Key Resources

- **R2 Bucket for Tofu Remote State**: A Cloudflare R2 bucket is created to serve as a centralized and secure backend for storing OpenTofu state files. It is configured with versioning and lifecycle rules for state protection and management.
- **S3-Compatible R2 Access Credentials**: This module programmatically generates a Cloudflare Account API Token that is specifically scoped to provide S3-compatible read and write access to the created R2 bucket. These credentials (Access Key ID and Access Key Secret) are then securely stored in Infisical.
- **Secrets Management with Infisical**: This module interacts with Infisical for managing sensitive configurations.
- **Input Secrets (Manually set up by the user in Infisical under the `/tofu` path in the appropriate Infisical environment):**
- `TF_VAR_cloudflare_master_account_api_token`: A Cloudflare API Account token with permissions to create R2 buckets and Account API tokens.
- `TF_VAR_cloudflare_account_id`: Your Cloudflare account ID.
- `TF_VAR_tofu_encryption_passphrase`: Encryption passphrase for encrypting the state file.
- The rest of the variables are automatically picked up from the devContainer environment.
- Note: If you create these variables after loading the devContainer, you will need to run `source ~/.zshrc` again to load them into the environment.
- **Other Secrets:**
- `TF_VAR_bucket_name` - automatically set in the devcontainer when set in the `.env` file in the root folder.
- `TF_VAR_branch_env`- automatically set in the devcontainer base on the current branch.
- `TF_VAR_tofu_encryption_passphrase` - set it in infisical manually
- `TF_VAR_infisical_domain` - automatically set in the devcontainer when set in the `.env` file in the root folder.
- `TF_VAR_infisical_client_id` - automatically set in the devcontainer when set in the `.env` file in the root folder.
- `TF_VAR_infisical_client_secret` - set it in [devcontainer](/.devcontainer/README.md) manually.
- `TF_VAR_infisical_project_id` - automatically set in the devcontainer when set in the `.env` file in the root folder.
- **Output Secrets (Automatically generated and stored by this Tofu module in Infisical under the path defined by `var.infisical_rw_secrets_path` (default: `/tofu_rw`) in the `prod` Infisical environment):**
- `TF_VAR_cloudflare_r2_tofu_access_key`: The S3-compatible Access Key ID for the R2 bucket.
- `TF_VAR_cloudflare_r2_tofu_access_secret`: The S3-compatible Secret Access Key for the R2 bucket.
- **OpenTofu State Encryption**: The OpenTofu state file (both local and remote) is encrypted at rest using a passphrase provided by the user. This ensures sensitive data within the state file is protected.

## Instructions

This module uses a two-phase approach for bootstrapping the remote state:

### Phase 1: Initial Apply (Local Backend)

Create the R2 bucket and generate access credentials using a local OpenTofu state.

```bash
# Copy local backend file
cp samples/backend_local.tofu.sample ./backend.tofu

# Initialize tofu
tofu init

# Run tofu apply to create R2 bucket and permissions
tofu apply
```

### Phase 2: Migrate State (Remote Backend)

Migrate your OpenTofu state to the newly provisioned R2 bucket.

```bash
# Copy R2 backend file
cp samples/backend_r2.tofu.sample ./backend.tofu

# Load Cloudflare tokens into the devcontainer environment
source ~/.zshrc

# If in local environment, you could run
# export TF_VAR_cloudflare_r2_tofu_access_key=$(tofu output -raw r2_access_key_id)
# export TF_VAR_cloudflare_r2_tofu_access_secret=$(tofu output -raw r2_secret_access_key)

# Re-initialize tofu to migrate state to R2 backend
# Double check TF_VAR_branch_env is properly set to your env - prod/staging/dev - everytime you checkout a new branch.
tofu init -migrate-state

# Remove any leftover local state files - careful, know what you are doing!
# rm *.tfstate*
```

Your OpenTofu state is now securely stored in the Cloudflare R2 bucket.
14 changes: 14 additions & 0 deletions tofu/remote-state/cf/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
## Overview

This OpenTofu module provisions the foundational infrastructure for managing OpenTofu state within Cloudflare R2. It automates the creation of the R2 bucket and generates the necessary S3-compatible access credentials for remote state storage.

## Key Resources

- **R2 Bucket for Tofu Remote State**: A Cloudflare R2 bucket with versioning and lifecycle rules for secure state storage.
- **S3-Compatible R2 Access Credentials**: Programmatically generated credentials (Access Key ID and Secret Access Key) for R2 access, securely stored in Infisical.
- **Secrets Management with Infisical**: Manages both input secrets (e.g., Cloudflare API token) and output secrets (e.g., R2 access credentials).
- **OpenTofu State Encryption**: Ensures the OpenTofu state file is encrypted at rest for enhanced security.

## Instructions

This module uses a two-phase approach for bootstrapping the remote state. For detailed instructions, including prerequisites and step-by-step guides, please refer to [DOCS.md](./DOCS.md).
18 changes: 18 additions & 0 deletions tofu/remote-state/cf/backend.tofu
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
terraform {
backend "s3" {
bucket = var.bucket_name
key = "remote-state/cf/${var.branch_env}/terraform.tfstate"
region = "auto"
skip_credentials_validation = true
skip_metadata_api_check = true
skip_region_validation = true
skip_requesting_account_id = true
skip_s3_checksum = true
use_path_style = true
endpoints = {
s3 = "https://${var.cloudflare_account_id}.r2.cloudflarestorage.com"
}
access_key = var.cloudflare_r2_tofu_access_key
secret_key = var.cloudflare_r2_tofu_access_secret
}
}
48 changes: 48 additions & 0 deletions tofu/remote-state/cf/cf.tofu
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
locals {
# Create a nested map: scope -> permission_name -> id
api_token_permission_groups_map = {
account = {
for perm in data.cloudflare_account_api_token_permission_groups_list.all.result :
perm.name => perm.id
if contains(perm.scopes, "com.cloudflare.api.account")
}
zone = {
for perm in data.cloudflare_account_api_token_permission_groups_list.all.result :
perm.name => perm.id
if contains(perm.scopes, "com.cloudflare.api.account.zone")
}
# Add R2 scope mapping
r2 = {
for perm in data.cloudflare_account_api_token_permission_groups_list.all.result :
perm.name => perm.id
if contains(perm.scopes, "com.cloudflare.edge.r2.bucket")
}
}
}

# Get API token permission groups data
data "cloudflare_account_api_token_permission_groups_list" "all" {
account_id = var.cloudflare_account_id
}

# Create R2 bucket for remote state
resource "cloudflare_r2_bucket" "tofu_bucket" {
account_id = var.cloudflare_account_id
name = var.bucket_name
}

# Create Account token for R2 access with proper permissions
resource "cloudflare_account_token" "r2_tofu_token" {
name = "R2 Tofu Remote State Token"
account_id = var.cloudflare_account_id

policies = [{
effect = "allow"
permission_groups = [{
id = local.api_token_permission_groups_map.r2["Workers R2 Storage Bucket Item Write"]
}]
resources = {
"com.cloudflare.edge.r2.bucket.${var.cloudflare_account_id}_default_${var.bucket_name}" = "*"
}
}]
}
29 changes: 29 additions & 0 deletions tofu/remote-state/cf/infisical.tofu
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
provider "infisical" {
host = var.infisical_domain
auth = {
universal = {
client_id = var.infisical_client_id
client_secret = var.infisical_client_secret
}
}
}

# Store R2 access key
resource "infisical_secret" "cloudflare_r2_tofu_access_key" {
name = "TF_VAR_cloudflare_r2_tofu_access_key"
value = cloudflare_account_token.r2_tofu_token.id

env_slug = var.branch_env
folder_path = var.infisical_rw_secrets_path
workspace_id = var.infisical_project_id
}

# Store R2 access secret
resource "infisical_secret" "cloudflare_r2_tofu_access_secret" {
name = "TF_VAR_cloudflare_r2_tofu_access_secret"
value = sha256(cloudflare_account_token.r2_tofu_token.value)

env_slug = var.branch_env
folder_path = var.infisical_rw_secrets_path
workspace_id = var.infisical_project_id
}
30 changes: 30 additions & 0 deletions tofu/remote-state/cf/infisical_variables.tofu
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
variable "infisical_domain" {
description = "Infisical Domain"
type = string
default = "https://app.infisical.com"
}

variable "infisical_client_id" {
description = "Infisical Client ID"
type = string
default = null
}

variable "infisical_project_id" {
description = "Infisical Project ID"
type = string
default = null
}

variable "infisical_rw_secrets_path" {
description = "Infisical Client Secret"
type = string
default = "/tofu_rw"
}

variable "infisical_client_secret" {
description = "Infisical Client Secret"
type = string
sensitive = true
default = null
}
38 changes: 38 additions & 0 deletions tofu/remote-state/cf/outputs.tofu
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
output "bucket_name" {
description = "The name of the R2 bucket."
value = cloudflare_r2_bucket.tofu_bucket.name
}

output "tofu_remote_state_token" {
description = "The API token for the Tofu remote state bucket."
value = {
id = cloudflare_account_token.r2_tofu_token.id
name = cloudflare_account_token.r2_tofu_token.name
}
sensitive = true
}

# R2 access credentials for S3-compatible operations
output "r2_access_key_id" {
description = "R2 access key ID for S3-compatible operations"
value = cloudflare_account_token.r2_tofu_token.id
sensitive = true
}

output "r2_secret_access_key" {
description = "R2 secret access key for S3-compatible operations"
value = cloudflare_account_token.r2_tofu_token.value
sensitive = true
}

# R2 endpoint for S3-compatible operations
output "r2_endpoint" {
description = "R2 S3-compatible endpoint"
value = "https://${var.cloudflare_account_id}.r2.cloudflarestorage.com"
}

# Bucket URL for direct access
output "bucket_url" {
description = "Direct URL to the R2 bucket"
value = "https://${var.cloudflare_account_id}.r2.cloudflarestorage.com/${var.bucket_name}"
}
32 changes: 32 additions & 0 deletions tofu/remote-state/cf/providers.tofu
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
terraform {
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = ">= 5.6.0"
}
infisical = {
source = "infisical/infisical"
version = ">= 0.15.21"
}
}
encryption {
key_provider "pbkdf2" "my_passphrase" {
passphrase = var.tofu_encryption_passphrase
}
method "aes_gcm" "my_method" {
keys = key_provider.pbkdf2.my_passphrase
}
state {
method = method.aes_gcm.my_method
enforced = true
}
plan {
method = method.aes_gcm.my_method
enforced = true
}
}
}

provider "cloudflare" {
api_token = var.cloudflare_master_account_api_token
}
5 changes: 5 additions & 0 deletions tofu/remote-state/cf/samples/backend_local.tofu.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
terraform {
backend "local" {
path = "${var.branch_env}.tfstate"
}
}
18 changes: 18 additions & 0 deletions tofu/remote-state/cf/samples/backend_r2.tofu.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
terraform {
backend "s3" {
bucket = var.bucket_name
key = "remote-state/cf/${var.branch_env}/terraform.tfstate"
region = "auto"
skip_credentials_validation = true
skip_metadata_api_check = true
skip_region_validation = true
skip_requesting_account_id = true
skip_s3_checksum = true
use_path_style = true
endpoints = {
s3 = "https://${var.cloudflare_account_id}.r2.cloudflarestorage.com"
}
access_key = var.cloudflare_r2_tofu_access_key
secret_key = var.cloudflare_r2_tofu_access_secret
}
}
53 changes: 53 additions & 0 deletions tofu/remote-state/cf/variables.tofu
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@

variable "cloudflare_account_id" {
type = string
description = "The Cloudflare account ID."
}

variable "cloudflare_master_account_api_token" {
type = string
description = "The Cloudflare Account API token for creating r2 bucket and access creds."
sensitive = true
}

variable "cloudflare_r2_tofu_access_key" {
type = string
description = "The Cloudflare R2 access key for tofu remote state."
default = null
}

variable "cloudflare_r2_tofu_access_secret" {
type = string
description = "The Cloudflare R2 access secret for tofu remote state."
default = null
}

# Define a variable for the bucket name.
# Set this in .env file in root, which should automatically set it in devcontainer env.
variable "bucket_name" {
description = "The globally unique name for the bucket."
type = string

validation {
condition = can(regex("^[a-z0-9][a-z0-9-]*[a-z0-9]$", var.bucket_name))
error_message = "Bucket name must be lowercase, alphanumeric, and hyphens only."
}

validation {
condition = length(var.bucket_name) >= 3 && length(var.bucket_name) <= 63
error_message = "Bucket name must be between 3 and 63 characters."
}
}

# Define a variable for part of bucket prefix.
# This should be set automatically based on branch logic in devcontainer.
variable "branch_env" {
description = "Part of bucket prefix."
type = string
}

variable "tofu_encryption_passphrase" {
description = "The encryption passphrase for tofu state encryption."
type = string
sensitive = true
}
Loading