[infra] Auto-create Cloudflare DNS records from ingress_factory

## Context

Deploying new services required manually adding hostnames to
cloudflare_proxied_names/cloudflare_non_proxied_names in config.tfvars —
a separate file from the service stack. This was frequently forgotten,
leaving services unreachable externally.

## This change:

- Add `dns_type` parameter to `ingress_factory` and `reverse_proxy/factory`
  modules. Setting `dns_type = "proxied"` or `"non-proxied"` auto-creates
  the Cloudflare DNS record (CNAME to tunnel or A/AAAA to public IP).
- Simplify cloudflared tunnel from 100 per-hostname rules to wildcard
  `*.viktorbarzin.me → Traefik`. Traefik still handles host-based routing.
- Add global Cloudflare provider via terragrunt.hcl (separate
  cloudflare_provider.tf with Vault-sourced API key).
- Migrate 118 hostnames from centralized config.tfvars to per-service
  dns_type. 17 hostnames remain centrally managed (Helm ingresses,
  special cases).
- Update docs, AGENTS.md, CLAUDE.md, dns.md runbook.

```
BEFORE                          AFTER
config.tfvars (manual list)     stacks/<svc>/main.tf
        |                         module "ingress" {
        v                           dns_type = "proxied"
stacks/cloudflared/               }
  for_each = list                     |
  cloudflare_record               auto-creates
  tunnel per-hostname             cloudflare_record + annotation
```

## What is NOT in this change:

- Uptime Kuma monitor migration (still reads from config.tfvars)
- 17 remaining centrally-managed hostnames (Helm, special cases)
- Removal of allow_overwrite (keep until migration confirmed stable)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Viktor Barzin 2026-04-16 13:45:04 +00:00
parent 95d2a6abf8
commit b1d152be1f
94 changed files with 471 additions and 34 deletions

View file

@ -1,3 +1,15 @@
terraform {
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = "~> 4"
}
kubernetes = {
source = "hashicorp/kubernetes"
}
}
}
variable "name" {}
variable "namespace" {
default = "reverse-proxy"
@ -45,6 +57,31 @@ variable "skip_global_rate_limit" {
type = bool
default = false
}
variable "dns_type" {
type = string
default = "none"
description = "Cloudflare DNS: 'proxied' (CNAME to tunnel), 'non-proxied' (A/AAAA to public IP), or 'none'"
validation {
condition = contains(["proxied", "non-proxied", "none"], var.dns_type)
error_message = "dns_type must be 'proxied', 'non-proxied', or 'none'."
}
}
variable "cloudflare_zone_id" {
type = string
default = "fd2c5dd4efe8fe38958944e74d0ced6d"
}
variable "cloudflare_tunnel_id" {
type = string
default = "75182cd7-bb91-4310-b961-5d8967da8b41"
}
variable "public_ip" {
type = string
default = "176.12.22.76"
}
variable "public_ipv6" {
type = string
default = "2001:470:6e:43d::2"
}
resource "kubernetes_service" "proxied-service" {
@ -88,7 +125,9 @@ resource "kubernetes_ingress_v1" "proxied-ingress" {
"traefik.ingress.kubernetes.io/router.entrypoints" = "websecure"
"traefik.ingress.kubernetes.io/service.serversscheme" = var.backend_protocol == "HTTPS" ? "https" : null
"traefik.ingress.kubernetes.io/service.serverstransport" = var.backend_protocol == "HTTPS" ? "traefik-insecure-skip-verify@kubernetescrd" : null
}, var.extra_annotations)
}, var.extra_annotations,
var.dns_type != "none" ? { "cloudflare.viktorbarzin.me/dns-type" = var.dns_type } : {}
)
}
spec {
@ -166,3 +205,37 @@ resource "kubernetes_manifest" "custom_csp" {
}
}
}
# Cloudflare DNS records created automatically when dns_type is set.
resource "cloudflare_record" "proxied" {
count = var.dns_type == "proxied" ? 1 : 0
name = var.name
content = "${var.cloudflare_tunnel_id}.cfargotunnel.com"
proxied = true
ttl = 1
type = "CNAME"
zone_id = var.cloudflare_zone_id
allow_overwrite = true
}
resource "cloudflare_record" "non_proxied_a" {
count = var.dns_type == "non-proxied" ? 1 : 0
name = var.name
content = var.public_ip
proxied = false
ttl = 1
type = "A"
zone_id = var.cloudflare_zone_id
allow_overwrite = true
}
resource "cloudflare_record" "non_proxied_aaaa" {
count = var.dns_type == "non-proxied" ? 1 : 0
name = var.name
content = var.public_ipv6
proxied = false
ttl = 1
type = "AAAA"
zone_id = var.cloudflare_zone_id
allow_overwrite = true
}

View file

@ -26,6 +26,7 @@ module "tls_secret" {
# https://pfsense.viktorbarzin.me/
module "pfsense" {
source = "./factory"
dns_type = "proxied"
name = "pfsense"
external_name = "pfsense.viktorbarzin.lan"
tls_secret_name = var.tls_secret_name
@ -53,6 +54,7 @@ module "pfsense" {
# https://nas.viktorbarzin.me/
module "nas" {
source = "./factory"
dns_type = "proxied"
name = "nas"
external_name = "nas.viktorbarzin.lan"
port = 5001
@ -74,6 +76,7 @@ module "nas" {
# https://files.viktorbarzin.me/
module "nas-files" {
source = "./factory"
dns_type = "non-proxied"
name = "files"
external_name = "nas.viktorbarzin.lan"
port = 5001
@ -89,6 +92,7 @@ module "nas-files" {
# https://idrac.viktorbarzin.me/
module "idrac" {
source = "./factory"
dns_type = "proxied"
name = "idrac"
external_name = "idrac.viktorbarzin.lan"
port = 443
@ -110,6 +114,7 @@ module "idrac" {
# TODO: Not working yet
module "tp-link-gateway" {
source = "./factory"
dns_type = "proxied"
name = "gw"
external_name = "gw.viktorbarzin.lan"
port = 443
@ -124,6 +129,7 @@ module "tp-link-gateway" {
# https://truenas.viktorbarzin.me/
module "truenas" {
source = "./factory"
dns_type = "proxied"
name = "truenas"
external_name = "truenas.viktorbarzin.lan"
port = 80
@ -168,6 +174,7 @@ module "r730" {
# https://proxmox.viktorbarzin.me/
module "proxmox" {
source = "./factory"
dns_type = "proxied"
name = "proxmox"
external_name = "proxmox.viktorbarzin.lan"
port = 8006
@ -189,6 +196,7 @@ module "proxmox" {
# https://docker.viktorbarzin.me/ (registry web UI)
module "docker-registry-ui" {
source = "./factory"
dns_type = "proxied"
name = "docker"
external_name = "docker-registry.viktorbarzin.lan"
port = 8080
@ -209,6 +217,7 @@ module "docker-registry-ui" {
# https://registry.viktorbarzin.me/ (Docker CLI push/pull endpoint)
module "docker-registry-cli" {
source = "./factory"
dns_type = "non-proxied"
name = "registry"
external_name = "docker-registry.viktorbarzin.lan"
port = 5050
@ -228,6 +237,7 @@ module "docker-registry-cli" {
# https://valchedrym.viktorbarzin.me/
module "valchedrym" {
source = "./factory"
dns_type = "proxied"
name = "valchedrym"
external_name = "valchedrym.viktorbarzin.lan"
tls_secret_name = var.tls_secret_name
@ -293,6 +303,7 @@ resource "kubernetes_manifest" "ha_sofia_rate_limit" {
module "ha-sofia" {
source = "./factory"
dns_type = "non-proxied"
name = "ha-sofia"
external_name = "ha-sofia.viktorbarzin.lan"
port = 8123
@ -317,6 +328,7 @@ module "ha-sofia" {
# https://music-assistant.viktorbarzin.me/
module "music-assistant" {
source = "./factory"
dns_type = "non-proxied"
name = "music-assistant"
external_name = "ha-sofia.viktorbarzin.lan"
port = 8095
@ -332,6 +344,7 @@ module "music-assistant" {
# https://ha-london.viktorbarzin.me/
module "ha-london" {
source = "./factory"
dns_type = "non-proxied"
name = "ha-london"
external_name = "ha-london.viktorbarzin.lan"
port = 8123
@ -351,6 +364,7 @@ module "ha-london" {
# https://london.viktorbarzin.me/
module "london" {
source = "./factory"
dns_type = "proxied"
name = "london"
external_name = "openwrt-london.viktorbarzin.lan"
port = 443
@ -374,6 +388,7 @@ module "london" {
}
module "pi-lights" {
source = "./factory"
dns_type = "proxied"
name = "pi"
external_name = "ha-london.viktorbarzin.lan"
port = 5000
@ -401,6 +416,7 @@ module "pi-lights" {
module "mbp14" {
source = "./factory"
dns_type = "proxied"
name = "mbp14"
external_name = "mbp14.viktorbarzin.lan"
port = 4020