feat: add DOKS, managed postgres/redis, WAF rules, external-dns
Some checks failed
Terraform / terraform (push) Failing after 32s
Test / test (push) Successful in 1s

This commit is contained in:
Ryan Moon
2026-04-02 17:25:13 -05:00
parent 155ef0345e
commit 3471374cb6
9 changed files with 195 additions and 8 deletions

30
terraform/doks.tf Normal file
View File

@@ -0,0 +1,30 @@
# ─── DOKS Cluster ─────────────────────────────────────────────────────────────
resource "digitalocean_kubernetes_cluster" "main" {
name = "lunarfront"
region = var.region
version = var.k8s_version
node_pool {
name = "workers"
size = var.k8s_node_size
min_nodes = var.k8s_min_nodes
max_nodes = var.k8s_max_nodes
auto_scale = true
}
tags = ["lunarfront", "k8s"]
}
# ─── DNS — wildcard for customer subdomains → cluster load balancer ───────────
# Uncomment after the cluster is up and nginx ingress load balancer IP is known.
# Set cluster_lb_ip in terraform.tfvars then re-run terraform apply.
resource "cloudflare_record" "apps_wildcard" {
zone_id = data.cloudflare_zone.main.id
name = "*"
type = "A"
content = var.cluster_lb_ip
proxied = true
ttl = 1
}

View File

@@ -112,11 +112,11 @@ resource "digitalocean_firewall" "gitea" {
source_addresses = ["0.0.0.0/0", "::/0"]
}
# Gitea SSH for git push/pull — your IP only
# Gitea SSH for git push/pull — open until Gitea is migrated to DOKS
inbound_rule {
protocol = "tcp"
port_range = "2222"
source_addresses = ["${var.admin_ip}/32"]
source_addresses = ["0.0.0.0/0", "::/0"]
}
outbound_rule {
@@ -138,7 +138,7 @@ resource "cloudflare_record" "gitea" {
zone_id = data.cloudflare_zone.main.id
name = "git"
type = "A"
value = digitalocean_droplet.gitea.ipv4_address
content = digitalocean_droplet.gitea.ipv4_address
proxied = true
ttl = 1
}
@@ -147,7 +147,7 @@ resource "cloudflare_record" "vaultwarden" {
zone_id = data.cloudflare_zone.main.id
name = "vault"
type = "A"
value = digitalocean_droplet.gitea.ipv4_address
content = digitalocean_droplet.gitea.ipv4_address
proxied = true
ttl = 1
}
@@ -157,7 +157,7 @@ resource "cloudflare_record" "git_ssh" {
zone_id = data.cloudflare_zone.main.id
name = "git-ssh"
type = "A"
value = digitalocean_droplet.gitea.ipv4_address
content = digitalocean_droplet.gitea.ipv4_address
proxied = false
ttl = 3600
}
@@ -167,7 +167,7 @@ resource "cloudflare_record" "registry" {
zone_id = data.cloudflare_zone.main.id
name = "registry"
type = "A"
value = digitalocean_droplet.gitea.ipv4_address
content = digitalocean_droplet.gitea.ipv4_address
proxied = false
ttl = 3600
}

View File

@@ -2,3 +2,28 @@ output "gitea_ip" {
description = "Public IP of the Gitea droplet"
value = digitalocean_droplet.gitea.ipv4_address
}
output "k8s_cluster_id" {
description = "DOKS cluster ID"
value = digitalocean_kubernetes_cluster.main.id
}
output "postgres_host" {
description = "Managed Postgres host"
value = digitalocean_database_cluster.postgres.host
}
output "postgres_port" {
description = "Managed Postgres port"
value = digitalocean_database_cluster.postgres.port
}
output "redis_host" {
description = "Managed Redis/Valkey host"
value = digitalocean_database_cluster.redis.host
}
output "redis_port" {
description = "Managed Redis/Valkey port"
value = digitalocean_database_cluster.redis.port
}

23
terraform/postgres.tf Normal file
View File

@@ -0,0 +1,23 @@
# ─── Managed PostgreSQL cluster ───────────────────────────────────────────────
# Shared across all customers — each customer gets their own database.
resource "digitalocean_database_cluster" "postgres" {
name = "lunarfront-postgres"
engine = "pg"
version = "16"
size = var.postgres_size
region = var.region
node_count = 1
tags = ["lunarfront", "postgres"]
}
# Restrict access to the DOKS cluster only
resource "digitalocean_database_firewall" "postgres" {
cluster_id = digitalocean_database_cluster.postgres.id
rule {
type = "k8s"
value = digitalocean_kubernetes_cluster.main.id
}
}

15
terraform/project.tf Normal file
View File

@@ -0,0 +1,15 @@
# ─── DO Project assignment ────────────────────────────────────────────────────
data "digitalocean_project" "main" {
name = "lunarfront-infra"
}
resource "digitalocean_project_resources" "main" {
project = data.digitalocean_project.main.id
resources = [
digitalocean_droplet.gitea.urn,
digitalocean_kubernetes_cluster.main.urn,
digitalocean_database_cluster.postgres.urn,
digitalocean_database_cluster.redis.urn,
]
}

22
terraform/redis.tf Normal file
View File

@@ -0,0 +1,22 @@
# ─── Managed Valkey/Redis cluster ─────────────────────────────────────────────
# Shared across all customers.
resource "digitalocean_database_cluster" "redis" {
name = "lunarfront-redis"
engine = "valkey"
version = "8"
size = var.redis_size
region = var.region
node_count = 1
tags = ["lunarfront", "redis"]
}
resource "digitalocean_database_firewall" "redis" {
cluster_id = digitalocean_database_cluster.redis.id
rule {
type = "k8s"
value = digitalocean_kubernetes_cluster.main.id
}
}

View File

@@ -1,6 +1,7 @@
do_token = "your-digitalocean-api-token"
ssh_key_name = "your-key-name-in-do"
region = "nyc3"
droplet_size = "s-1vcpu-2gb"
droplet_size = "s-2vcpu-4gb"
cloudflare_api_token = "your-cloudflare-api-token"
domain = "example.com"
cluster_lb_ip = "your-ingress-lb-ip"

View File

@@ -18,7 +18,7 @@ variable "region" {
variable "droplet_size" {
description = "Droplet size slug"
type = string
default = "s-1vcpu-2gb"
default = "s-2vcpu-4gb"
}
variable "cloudflare_api_token" {
@@ -37,3 +37,49 @@ variable "admin_ip" {
type = string
sensitive = true
}
# ─── DOKS ─────────────────────────────────────────────────────────────────────
variable "k8s_version" {
description = "Kubernetes version slug (run: doctl kubernetes options versions)"
type = string
default = "1.32.13-do.2"
}
variable "k8s_node_size" {
description = "Node pool droplet size"
type = string
default = "s-2vcpu-4gb"
}
variable "k8s_min_nodes" {
description = "Minimum nodes in the pool"
type = number
default = 1
}
variable "k8s_max_nodes" {
description = "Maximum nodes in the pool"
type = number
default = 3
}
variable "cluster_lb_ip" {
description = "IP of the DOKS ingress load balancer (set after first cluster apply)"
type = string
default = ""
}
# ─── Managed databases ────────────────────────────────────────────────────────
variable "postgres_size" {
description = "Managed Postgres cluster size slug"
type = string
default = "db-s-1vcpu-1gb"
}
variable "redis_size" {
description = "Managed Valkey/Redis cluster size slug"
type = string
default = "db-s-1vcpu-1gb"
}

25
terraform/waf.tf Normal file
View File

@@ -0,0 +1,25 @@
# ─── Cloudflare WAF — restrict admin subdomains to admin IP ───────────────────
resource "cloudflare_ruleset" "admin_ip_allowlist" {
zone_id = data.cloudflare_zone.main.id
name = "Admin IP allowlist"
description = "Block access to admin subdomains from non-admin IPs"
kind = "zone"
phase = "http_request_firewall_custom"
rules {
action = "block"
description = "Block non-admin IPs from admin subdomains"
enabled = true
expression = <<-EOT
(
http.host in {
"git.${var.domain}"
"vault.${var.domain}"
"argocd.${var.domain}"
}
and not ip.src eq ${var.admin_ip}
)
EOT
}
}