Skip to content
Merged
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
4 changes: 4 additions & 0 deletions shell.nix
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ pkgs.mkShell {
pkgs.xz
pkgs.libxml2
pkgs.pkg-config

# Terraform
pkgs.terraform
pkgs.wrangler
]
++ pkgs.lib.optional with-blackfire pkgs.blackfire
;
Expand Down
31 changes: 31 additions & 0 deletions terraform/cloudflare/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Terraform state files
*.tfstate
*.tfstate.*

# Terraform variable files with sensitive data
*.auto.tfvars

# Terraform CLI configuration files
.terraformrc
terraform.rc

# Terraform directory
.terraform/

# Wrangler / Workers local development
workers/.dev.vars
workers/.wrangler/
.terraform.lock.hcl

# Crash log files
crash.log
crash.*.log

# Override files
override.tf
override.tf.json
*_override.tf
*_override.tf.json

# Plans
*.tfplan
15 changes: 15 additions & 0 deletions terraform/cloudflare/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Cloudflare Terraform

Required Variables:

```
export CLOUDFLARE_API_TOKEN="xxx"
export TF_VAR_account_id="xxx"
export TF_VAR_zone_id="xxx"
```

# Initialization

```
terraform init
```
7 changes: 7 additions & 0 deletions terraform/cloudflare/dns.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# DNS records for flow-php.com zone

# This file is intentionally minimal - the R2 custom domain resource
# automatically creates the necessary DNS records for playground-snippets.flow-php.com
# via cloudflare_r2_custom_domain resource in r2.tf

# Add additional DNS records below as needed
21 changes: 21 additions & 0 deletions terraform/cloudflare/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
resource "cloudflare_account" "account" {
name = "norbert@orzechowicz.pl"
type = "standard"
}

import {
id = var.account_id
to = cloudflare_account.account
}

resource "cloudflare_zone" "flow_php" {
account = cloudflare_account.account
name = "flow-php.com"

paused = false
}

import {
id = var.zone_id
to = cloudflare_zone.flow_php
}
62 changes: 62 additions & 0 deletions terraform/cloudflare/notifications.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
locals {
k = 1000
m = 1000 * local.k
mb = 1024 * 1024
gb = 1024 * 1024 * 1024

r2_storage_free_tier = 10 * local.gb # 10 GB
r2_class_a_operations_free_tier = 1 * local.m # 1 million requests
r2_class_b_operations_free_tier = 10 * local.m # 10 million requests

thresholds = [10, 20, 30, 40, 50, 60, 70, 80, 90, 95]

notification_configs = merge(
{
for threshold in local.thresholds :
"r2_storage_${threshold}" => {
name = "R2 Storage - ${threshold}% of Free Tier"
product = "r2_storage"
limit = local.r2_storage_free_tier * (threshold / 100)
description = "Alert when R2 storage usage reaches ${threshold}% of free tier (${threshold / 10} GB)"
}
},
{
for threshold in local.thresholds :
"r2_class_a_${threshold}" => {
name = "R2 Class A Operations - ${threshold}% of Free Tier"
product = "r2_class_a_operations"
limit = local.r2_class_a_operations_free_tier * (threshold / 100)
description = "Alert when R2 Class A operations reach ${threshold}% of free tier (${threshold * 10000} requests)"
}
},
{
for threshold in local.thresholds :
"r2_class_b_${threshold}" => {
name = "R2 Class B Operations - ${threshold}% of Free Tier"
product = "r2_class_b_operations"
limit = local.r2_class_b_operations_free_tier * (threshold / 100)
description = "Alert when R2 Class B operations reach ${threshold}% of free tier (${threshold * 100000} requests)"
}
}
)
}

resource "cloudflare_notification_policy" "r2_notifications" {
for_each = local.notification_configs

account_id = cloudflare_account.account.id
name = each.value.name
enabled = true
alert_type = "billing_usage_alert"

filters = {
limit = [each.value.limit]
product = [each.value.product]
}

mechanisms = {
email = [{
id = var.notification_email
}]
}
}
25 changes: 25 additions & 0 deletions terraform/cloudflare/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
output "turnstile_site_key" {
description = "Turnstile site key for Flow PHP Playground (public, use in frontend)"
value = cloudflare_turnstile_widget.flow_php.id
}

output "turnstile_secret_key" {
description = "Turnstile secret key for Flow PHP Playground (private, use in backend)"
value = cloudflare_turnstile_widget.flow_php.secret
sensitive = true
}

output "r2_playground_snippets_bucket_name" {
description = "Name of the R2 bucket for playground snippets"
value = cloudflare_r2_bucket.playground_snippets.name
}

output "r2_playground_snippets_bucket_id" {
description = "ID of the R2 bucket for playground snippets"
value = cloudflare_r2_bucket.playground_snippets.id
}

output "r2_playground_snippets_custom_domain" {
description = "Custom domain URL for R2 playground snippets bucket"
value = "https://playground-snippets.flow-php.com"
}
2 changes: 2 additions & 0 deletions terraform/cloudflare/provider.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
provider "cloudflare" {
}
29 changes: 29 additions & 0 deletions terraform/cloudflare/r2.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
resource "cloudflare_r2_bucket" "playground_snippets" {
account_id = cloudflare_account.account.id
name = "flow-php-playground-snippets"
location = "eeur"
}

resource "cloudflare_r2_bucket_cors" "playground_snippets_cors" {
account_id = cloudflare_account.account.id
bucket_name = cloudflare_r2_bucket.playground_snippets.name

rules = [{
allowed = {
methods = ["GET", "PUT", "POST", "DELETE", "HEAD"]
origins = ["https://flow-php.com"]
headers = ["*"]
}
id = "playground-snippets-cors"
expose_headers = ["ETag"]
max_age_seconds = 3600
}]
}

resource "cloudflare_r2_custom_domain" "playground_snippets" {
account_id = cloudflare_account.account.id
bucket_name = cloudflare_r2_bucket.playground_snippets.name
domain = "playground-snippets.flow-php.com"
zone_id = cloudflare_zone.flow_php.id
enabled = true
}
7 changes: 7 additions & 0 deletions terraform/cloudflare/turnstile.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
resource "cloudflare_turnstile_widget" "flow_php" {
account_id = cloudflare_account.account.id
name = "Flow PHP"
domains = ["flow-php.com", "www.flow-php.com"]
mode = "managed"
region = "world"
}
16 changes: 16 additions & 0 deletions terraform/cloudflare/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
variable "account_id" {
description = "Cloudflare Account ID"
type = string
}

variable "zone_id" {
description = "Cloudflare Zone ID"
type = string
}


variable "notification_email" {
description = "Email address for Cloudflare notifications"
type = string
default = "norbert@orzechowicz.pl"
}
10 changes: 10 additions & 0 deletions terraform/cloudflare/versions.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
terraform {
required_version = ">= 1.0"

required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = "5.12.0"
}
}
}
28 changes: 28 additions & 0 deletions terraform/cloudflare/waf.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
resource "cloudflare_ruleset" "playground_bot_protection" {
zone_id = cloudflare_zone.flow_php.id
name = "Playground Bot Protection"
description = "Bot protection for playground route"
kind = "zone"
phase = "http_request_firewall_custom"

rules = [
{
action = "block"
expression = "(http.request.uri.path contains \"/playground\" and ip.geoip.country in {\"CN\" \"RU\" \"VN\" \"IN\" \"BR\" \"ID\"})"
description = "Block playground access from CN, RU, VN, IN, BR, ID"
enabled = true
},
{
action = "managed_challenge"
expression = "(http.request.uri.path contains \"/playground\")"
description = "Managed challenge for playground access"
enabled = true
},
{
action = "challenge"
expression = "(http.request.uri.path contains \"/playground\" and cf.threat_score gt 14)"
description = "Challenge IPs with bad reputation accessing playground"
enabled = true
}
]
}
17 changes: 17 additions & 0 deletions terraform/cloudflare/workers.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
resource "cloudflare_workers_script" "turnstile_verify" {
account_id = cloudflare_account.account.id
script_name = "turnstile-verify"
content = file("${path.module}/workers/turnstile-verify.js")

bindings = [{
name = "TURNSTILE_SECRET_KEY"
type = "secret_text"
text = cloudflare_turnstile_widget.flow_php.secret
}]
}

resource "cloudflare_workers_route" "turnstile_verify" {
zone_id = cloudflare_zone.flow_php.id
pattern = "flow-php.com/api/verify-turnstile"
script = cloudflare_workers_script.turnstile_verify.script_name
}
4 changes: 4 additions & 0 deletions terraform/cloudflare/workers/.dev.vars.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Copy this file to .dev.vars and fill in your actual Turnstile secret key
# .dev.vars is gitignored and used for local development with wrangler

TURNSTILE_SECRET_KEY=your-turnstile-secret-key-here
2 changes: 2 additions & 0 deletions terraform/cloudflare/workers/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
.wrangler
Loading