From e6b0294a632053c13d5d0ea6156ef47b842e92bb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 04:45:25 +0000 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=A4=96=20chore(tofu):=20Update=20clou?= =?UTF-8?q?dflare/cloudflare=20requirement=20in=20/infra/tofu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates the requirements on [cloudflare/cloudflare](https://github.com/cloudflare/terraform-provider-cloudflare) to permit the latest version. - [Release notes](https://github.com/cloudflare/terraform-provider-cloudflare/releases) - [Changelog](https://github.com/cloudflare/terraform-provider-cloudflare/blob/main/CHANGELOG.md) - [Commits](https://github.com/cloudflare/terraform-provider-cloudflare/compare/v4.52.0...v5.26.0) --- updated-dependencies: - dependency-name: cloudflare/cloudflare dependency-version: 5.26.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- infra/tofu/versions.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/tofu/versions.tf b/infra/tofu/versions.tf index fa6d653..23804c0 100644 --- a/infra/tofu/versions.tf +++ b/infra/tofu/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { cloudflare = { source = "cloudflare/cloudflare" - version = "~> 4.52" + version = "~> 5.26" } } From 307cf48b4bb18cc106c57c92e7bdbeb270d3a6d2 Mon Sep 17 00:00:00 2001 From: Simon Thorley Date: Sun, 18 Oct 2026 18:36:28 +0100 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=A4=96=20chore(tofu):=20adapt=20infra?= =?UTF-8?q?structure=20code=20for=20Cloudflare=20provider=20v5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Convert cloudflare_pages_project blocks to attributes and migrate deployment_configs environment variables to typed env_vars maps - Rename cloudflare_pages_domain 'domain' attribute to 'name' - Rename cloudflare_record to cloudflare_dns_record and update outputs.tf hostname reference - Migrate application-scoped access policy into inline policies on cloudflare_zero_trust_access_application - Update docs/STAGED_ROLLOUT_PLAN.md architecture references Developed with AI assistant assistance (Google Deepmind Antigravity). Verified via tofu validate, tofu fmt, and mock tofu plan. --- docs/STAGED_ROLLOUT_PLAN.md | 4 +- infra/tofu/main.tf | 134 ++++++++++++++++++++---------------- infra/tofu/outputs.tf | 4 +- 3 files changed, 80 insertions(+), 62 deletions(-) diff --git a/docs/STAGED_ROLLOUT_PLAN.md b/docs/STAGED_ROLLOUT_PLAN.md index 7343934..8cbf694 100644 --- a/docs/STAGED_ROLLOUT_PLAN.md +++ b/docs/STAGED_ROLLOUT_PLAN.md @@ -107,9 +107,9 @@ pre-commit run --all-files ### Directory Structure (`infra/tofu/`) ``` infra/tofu/ -├── versions.tf # OpenTofu requirement (>= 1.6.0), cloudflare/cloudflare (~> 4.52), local backend +├── versions.tf # OpenTofu requirement (>= 1.6.0), cloudflare/cloudflare (~> 5.26), local backend ├── variables.tf # Account ID, Zone ID, custom_domain (scbind.com), allowed_ips, allowed_emails -├── main.tf # cloudflare_pages_project, cloudflare_pages_domain, cloudflare_record, zero trust app & policy +├── main.tf # cloudflare_pages_project, cloudflare_pages_domain, cloudflare_dns_record, zero trust app & inline policy ├── outputs.tf # Live URLs, DNS CNAME, Zero Trust AUD tag, and staged rollout status └── terraform.tfvars.example # Git-tracked placeholder template ``` diff --git a/infra/tofu/main.tf b/infra/tofu/main.tf index 3c56d70..7b3f3fc 100644 --- a/infra/tofu/main.tf +++ b/infra/tofu/main.tf @@ -21,42 +21,71 @@ resource "cloudflare_pages_project" "app" { name = var.project_name production_branch = var.production_branch - source { + source = { type = "github" - config { - owner = local.repo_owner - repo_name = local.repo_name - production_branch = var.production_branch - pr_comments_enabled = true - deployments_enabled = true - production_deployment_enabled = true - preview_deployment_setting = "all" + config = { + owner = local.repo_owner + repo_name = local.repo_name + production_branch = var.production_branch + pr_comments_enabled = true + production_deployments_enabled = true + preview_deployment_setting = "all" } } - build_config { + build_config = { build_command = var.build_command destination_dir = var.build_output_directory root_dir = var.root_directory } - deployment_configs { - preview { - environment_variables = { - NODE_VERSION = var.node_version - VITE_MONETIZATION_MODE = "test" - VITE_SUPPORTER_URL = var.supporter_url - VITE_AFFILIATE_AMAZON_TAG = var.affiliate_amazon_tag + deployment_configs = { + preview = { + env_vars = { + NODE_VERSION = { + type = "plain_text" + value = var.node_version + } + VITE_MONETIZATION_MODE = { + type = "plain_text" + value = "test" + } + VITE_SUPPORTER_URL = { + type = "plain_text" + value = var.supporter_url + } + VITE_AFFILIATE_AMAZON_TAG = { + type = "plain_text" + value = var.affiliate_amazon_tag + } } } - production { - environment_variables = { - NODE_VERSION = var.node_version - VITE_MONETIZATION_MODE = var.monetization_mode != "" ? var.monetization_mode : (var.enable_zero_trust ? "test" : "live") - VITE_SUPPORTER_URL = var.supporter_url - VITE_AFFILIATE_AMAZON_TAG = var.affiliate_amazon_tag - VITE_ADSENSE_CLIENT_ID = var.adsense_client_id - VITE_ADSENSE_SLOT_ID = var.adsense_slot_id + production = { + env_vars = { + NODE_VERSION = { + type = "plain_text" + value = var.node_version + } + VITE_MONETIZATION_MODE = { + type = "plain_text" + value = var.monetization_mode != "" ? var.monetization_mode : (var.enable_zero_trust ? "test" : "live") + } + VITE_SUPPORTER_URL = { + type = "plain_text" + value = var.supporter_url + } + VITE_AFFILIATE_AMAZON_TAG = { + type = "plain_text" + value = var.affiliate_amazon_tag + } + VITE_ADSENSE_CLIENT_ID = { + type = "plain_text" + value = var.adsense_client_id + } + VITE_ADSENSE_SLOT_ID = { + type = "plain_text" + value = var.adsense_slot_id + } } } } @@ -69,12 +98,12 @@ resource "cloudflare_pages_project" "app" { resource "cloudflare_pages_domain" "custom_domain" { account_id = var.cloudflare_account_id project_name = cloudflare_pages_project.app.name - domain = var.custom_domain + name = var.custom_domain } # Configure proxied DNS CNAME record pointing custom_domain to .pages.dev. # For apex domains (scbind.com), Cloudflare proxy automatically provides CNAME flattening. -resource "cloudflare_record" "pages_cname" { +resource "cloudflare_dns_record" "pages_cname" { zone_id = var.cloudflare_zone_id name = var.custom_domain content = "${cloudflare_pages_project.app.name}.pages.dev" @@ -89,52 +118,41 @@ resource "cloudflare_record" "pages_cname" { ] } + # ============================================================================== # 3. Gated Access & Staged Rollout (Cloudflare Zero Trust) # ============================================================================== # Cloudflare Access Application protecting the custom domain. # When var.enable_zero_trust = false (Public Launch), this resource and its policy # are destroyed, allowing unrestricted public traffic. -resource "cloudflare_zero_trust_access_application" "pages_app" { - count = var.enable_zero_trust ? 1 : 0 - account_id = var.cloudflare_account_id - name = "${var.project_name} Staged Access Gate" - domain = var.custom_domain - type = "self_hosted" - session_duration = var.session_duration -} - +# # Staged Access Policy: # - Stage 2A: IP Whitelist — allows seamless access from your public IP / initial tester IPs. # - Stage 2B: Email Allowlist — permits invited friends to authenticate via Cloudflare Access # One-Time PIN (OTP) without needing static IP addresses. # -# Cloudflare Access combines multiple 'include' blocks using OR logic: +# Cloudflare Access evaluates multiple 'include' entries using OR logic: # A visitor is granted access if their IP matches 'allowed_ips' OR they authenticate # with an email in 'allowed_emails'. -resource "cloudflare_zero_trust_access_policy" "staged_access" { - count = var.enable_zero_trust ? 1 : 0 - account_id = var.cloudflare_account_id - application_id = cloudflare_zero_trust_access_application.pages_app[0].id - name = "${var.project_name} Staged Access Policy" - decision = "allow" - precedence = 1 - - # Stage 2A: IP Whitelist selector - dynamic "include" { - for_each = length(var.allowed_ips) > 0 ? [1] : [] - content { - ip = var.allowed_ips - } - } +resource "cloudflare_zero_trust_access_application" "pages_app" { + count = var.enable_zero_trust ? 1 : 0 + account_id = var.cloudflare_account_id + name = "${var.project_name} Staged Access Gate" + domain = var.custom_domain + type = "self_hosted" + session_duration = var.session_duration - # Stage 2B: Email OTP Allowlist selector - dynamic "include" { - for_each = length(var.allowed_emails) > 0 ? [1] : [] - content { - email = var.allowed_emails + policies = [ + { + name = "${var.project_name} Staged Access Policy" + decision = "allow" + precedence = 1 + include = concat( + [for ip in var.allowed_ips : { ip = { ip = ip } }], + [for email in var.allowed_emails : { email = { email = email } }] + ) } - } + ] lifecycle { precondition { diff --git a/infra/tofu/outputs.tf b/infra/tofu/outputs.tf index b3a2da8..d97a869 100644 --- a/infra/tofu/outputs.tf +++ b/infra/tofu/outputs.tf @@ -20,12 +20,12 @@ output "custom_domain_url" { output "dns_record_id" { description = "The ID of the proxied DNS CNAME record." - value = cloudflare_record.pages_cname.id + value = cloudflare_dns_record.pages_cname.id } output "dns_record_hostname" { description = "The fully-qualified hostname configured in DNS." - value = cloudflare_record.pages_cname.hostname + value = cloudflare_dns_record.pages_cname.name } output "zero_trust_enabled" {