feat: Add private network architecture with NAT gateway
Enable deployment of client servers without public IPs using private network (10.0.0.0/16) with NAT gateway via edge server. ## Infrastructure Changes: ### Terraform (tofu/): - **network.tf**: Define private network and subnet (10.0.0.0/24) - NAT gateway route through edge server - Firewall rules for client servers - **main.tf**: Support private-only servers - Optional public_ip_enabled flag per client - Dynamic network block for private IP assignment - User-data templates for public vs private servers - **user-data-*.yml**: Cloud-init templates - Private servers: Configure default route via NAT gateway - Public servers: Standard configuration - **dns.tf**: Update DNS to support edge routing - Client domains point to edge server IP - Wildcard DNS for subdomains - **variables.tf**: Add private_ip and public_ip_enabled options ### Ansible: - **deploy.yml**: Add diun and kuma roles to deployment ## Benefits: - Cost savings: No public IP needed for each client - Scalability: No public IP exhaustion limits - Security: Clients not directly exposed to internet - Centralized SSL: All TLS termination at edge 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
13685eb454
commit
79635eeece
7 changed files with 172 additions and 20 deletions
|
|
@ -66,6 +66,10 @@
|
||||||
- role: mailgun
|
- role: mailgun
|
||||||
- role: authentik
|
- role: authentik
|
||||||
- role: nextcloud
|
- role: nextcloud
|
||||||
|
- role: diun
|
||||||
|
tags: diun
|
||||||
|
- role: kuma
|
||||||
|
tags: kuma
|
||||||
|
|
||||||
post_tasks:
|
post_tasks:
|
||||||
- name: Display deployment summary
|
- name: Display deployment summary
|
||||||
|
|
|
||||||
18
tofu/dns.tf
18
tofu/dns.tf
|
|
@ -6,7 +6,8 @@ data "hcloud_zone" "main" {
|
||||||
name = var.base_domain
|
name = var.base_domain
|
||||||
}
|
}
|
||||||
|
|
||||||
# A Records for client servers (e.g., test.vrije.cloud -> 78.47.191.38)
|
# A Records for client servers with public IPs (e.g., test.vrije.cloud -> 78.47.191.38)
|
||||||
|
# Clients without public IPs (behind edge proxy) point to edge server instead
|
||||||
resource "hcloud_zone_rrset" "client_a" {
|
resource "hcloud_zone_rrset" "client_a" {
|
||||||
for_each = var.clients
|
for_each = var.clients
|
||||||
|
|
||||||
|
|
@ -16,8 +17,8 @@ resource "hcloud_zone_rrset" "client_a" {
|
||||||
ttl = 300
|
ttl = 300
|
||||||
records = [
|
records = [
|
||||||
{
|
{
|
||||||
value = hcloud_server.client[each.key].ipv4_address
|
value = lookup(each.value, "public_ip_enabled", true) ? hcloud_server.client[each.key].ipv4_address : hcloud_server.edge.ipv4_address
|
||||||
comment = "Client ${each.key} server"
|
comment = lookup(each.value, "public_ip_enabled", true) ? "Client ${each.key} server" : "Client ${each.key} via edge proxy"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -32,15 +33,18 @@ resource "hcloud_zone_rrset" "client_wildcard" {
|
||||||
ttl = 300
|
ttl = 300
|
||||||
records = [
|
records = [
|
||||||
{
|
{
|
||||||
value = hcloud_server.client[each.key].ipv4_address
|
value = lookup(each.value, "public_ip_enabled", true) ? hcloud_server.client[each.key].ipv4_address : hcloud_server.edge.ipv4_address
|
||||||
comment = "Wildcard for ${each.key} subdomains (Zitadel, Nextcloud, etc)"
|
comment = lookup(each.value, "public_ip_enabled", true) ? "Wildcard for ${each.key} subdomains" : "Wildcard for ${each.key} via edge proxy"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
# AAAA Records for IPv6 (e.g., test.vrije.cloud IPv6)
|
# AAAA Records for IPv6 (only for servers with public IPs)
|
||||||
resource "hcloud_zone_rrset" "client_aaaa" {
|
resource "hcloud_zone_rrset" "client_aaaa" {
|
||||||
for_each = var.clients
|
for_each = {
|
||||||
|
for k, v in var.clients : k => v
|
||||||
|
if lookup(v, "public_ip_enabled", true)
|
||||||
|
}
|
||||||
|
|
||||||
zone = data.hcloud_zone.main.name
|
zone = data.hcloud_zone.main.name
|
||||||
name = each.value.subdomain
|
name = each.value.subdomain
|
||||||
|
|
|
||||||
33
tofu/main.tf
33
tofu/main.tf
|
|
@ -70,18 +70,25 @@ resource "hcloud_server" "client" {
|
||||||
# Enable backups if requested
|
# Enable backups if requested
|
||||||
backups = var.enable_snapshots
|
backups = var.enable_snapshots
|
||||||
|
|
||||||
|
# Public network configuration (can be disabled for private-only servers)
|
||||||
|
public_net {
|
||||||
|
ipv4_enabled = lookup(each.value, "public_ip_enabled", true)
|
||||||
|
ipv6_enabled = lookup(each.value, "public_ip_enabled", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Private network (required for servers without public IP)
|
||||||
|
dynamic "network" {
|
||||||
|
for_each = lookup(each.value, "private_ip", null) != null ? [1] : []
|
||||||
|
content {
|
||||||
|
network_id = hcloud_network.private.id
|
||||||
|
ip = each.value.private_ip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# User data for initial setup
|
# User data for initial setup
|
||||||
user_data = <<-EOF
|
user_data = lookup(each.value, "public_ip_enabled", true) == false ? templatefile("${path.module}/user-data-private.yml", {
|
||||||
#cloud-config
|
hostname = each.key
|
||||||
package_update: true
|
}) : templatefile("${path.module}/user-data-public.yml", {
|
||||||
package_upgrade: true
|
hostname = each.key
|
||||||
packages:
|
})
|
||||||
- curl
|
|
||||||
- wget
|
|
||||||
- git
|
|
||||||
- python3
|
|
||||||
- python3-pip
|
|
||||||
runcmd:
|
|
||||||
- hostnamectl set-hostname ${each.key}
|
|
||||||
EOF
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
86
tofu/network.tf
Normal file
86
tofu/network.tf
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
# Private Network Configuration
|
||||||
|
# Enables client servers to communicate without public IPs
|
||||||
|
|
||||||
|
# Private Network
|
||||||
|
resource "hcloud_network" "private" {
|
||||||
|
name = "client-private-network"
|
||||||
|
ip_range = "10.0.0.0/16"
|
||||||
|
|
||||||
|
labels = {
|
||||||
|
managed = "terraform"
|
||||||
|
purpose = "client-internal"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Subnet for client servers
|
||||||
|
resource "hcloud_network_subnet" "clients" {
|
||||||
|
network_id = hcloud_network.private.id
|
||||||
|
type = "cloud"
|
||||||
|
network_zone = "eu-central"
|
||||||
|
ip_range = "10.0.0.0/24"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Note: Client servers attach to private network via main.tf dynamic block
|
||||||
|
|
||||||
|
# Edge Server Configuration
|
||||||
|
# Single public-facing reverse proxy for all clients
|
||||||
|
|
||||||
|
# SSH key for edge server
|
||||||
|
resource "hcloud_ssh_key" "edge" {
|
||||||
|
name = "edge-server-deploy-key"
|
||||||
|
public_key = file("${path.module}/../keys/ssh/edge.pub")
|
||||||
|
}
|
||||||
|
|
||||||
|
# Edge server (public IP + private network)
|
||||||
|
resource "hcloud_server" "edge" {
|
||||||
|
name = "edge"
|
||||||
|
server_type = var.edge_server_type
|
||||||
|
image = "ubuntu-24.04"
|
||||||
|
location = var.edge_location
|
||||||
|
ssh_keys = [hcloud_ssh_key.edge.id]
|
||||||
|
firewall_ids = [hcloud_firewall.client_firewall.id]
|
||||||
|
|
||||||
|
labels = {
|
||||||
|
role = "edge-proxy"
|
||||||
|
managed = "terraform"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Enable backups
|
||||||
|
backups = var.enable_snapshots
|
||||||
|
|
||||||
|
# User data for initial setup
|
||||||
|
user_data = <<-EOF
|
||||||
|
#cloud-config
|
||||||
|
package_update: true
|
||||||
|
package_upgrade: true
|
||||||
|
packages:
|
||||||
|
- curl
|
||||||
|
- wget
|
||||||
|
- git
|
||||||
|
- python3
|
||||||
|
- python3-pip
|
||||||
|
runcmd:
|
||||||
|
- hostnamectl set-hostname edge
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Ensure public network is enabled
|
||||||
|
public_net {
|
||||||
|
ipv4_enabled = true
|
||||||
|
ipv6_enabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Attach edge server to private network
|
||||||
|
resource "hcloud_server_network" "edge" {
|
||||||
|
server_id = hcloud_server.edge.id
|
||||||
|
network_id = hcloud_network.private.id
|
||||||
|
ip = "10.0.0.2" # Fixed IP for edge server (10.0.0.1 is gateway)
|
||||||
|
}
|
||||||
|
|
||||||
|
# NAT Gateway Route
|
||||||
|
# Routes all internet-bound traffic from private network through edge server
|
||||||
|
resource "hcloud_network_route" "nat_gateway" {
|
||||||
|
network_id = hcloud_network.private.id
|
||||||
|
destination = "0.0.0.0/0"
|
||||||
|
gateway = "10.0.0.2" # Edge server acts as NAT gateway
|
||||||
|
}
|
||||||
25
tofu/user-data-private.yml
Normal file
25
tofu/user-data-private.yml
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
#cloud-config
|
||||||
|
package_update: true
|
||||||
|
package_upgrade: true
|
||||||
|
packages:
|
||||||
|
- curl
|
||||||
|
- wget
|
||||||
|
- git
|
||||||
|
- python3
|
||||||
|
- python3-pip
|
||||||
|
runcmd:
|
||||||
|
- hostnamectl set-hostname ${hostname}
|
||||||
|
- |
|
||||||
|
# Configure default route for private-only server
|
||||||
|
# Hetzner network route forwards traffic to edge gateway (10.0.0.2)
|
||||||
|
cat > /etc/netplan/60-private-network.yaml <<'NETPLAN'
|
||||||
|
network:
|
||||||
|
version: 2
|
||||||
|
ethernets:
|
||||||
|
enp7s0:
|
||||||
|
routes:
|
||||||
|
- to: default
|
||||||
|
via: 10.0.0.1
|
||||||
|
NETPLAN
|
||||||
|
chmod 600 /etc/netplan/60-private-network.yaml
|
||||||
|
netplan apply
|
||||||
11
tofu/user-data-public.yml
Normal file
11
tofu/user-data-public.yml
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
#cloud-config
|
||||||
|
package_update: true
|
||||||
|
package_upgrade: true
|
||||||
|
packages:
|
||||||
|
- curl
|
||||||
|
- wget
|
||||||
|
- git
|
||||||
|
- python3
|
||||||
|
- python3-pip
|
||||||
|
runcmd:
|
||||||
|
- hostnamectl set-hostname ${hostname}
|
||||||
|
|
@ -31,10 +31,25 @@ variable "clients" {
|
||||||
subdomain = string # e.g., "alpha" for alpha.platform.nl
|
subdomain = string # e.g., "alpha" for alpha.platform.nl
|
||||||
apps = list(string) # e.g., ["zitadel", "nextcloud"]
|
apps = list(string) # e.g., ["zitadel", "nextcloud"]
|
||||||
nextcloud_volume_size = number # Size in GB for Nextcloud data volume (min 10, max 10000)
|
nextcloud_volume_size = number # Size in GB for Nextcloud data volume (min 10, max 10000)
|
||||||
|
private_ip = optional(string) # Private IP in 10.0.0.0/24 range (e.g., "10.0.0.10")
|
||||||
|
public_ip_enabled = optional(bool, true) # Whether to enable public IP (default: true for backward compatibility)
|
||||||
}))
|
}))
|
||||||
default = {}
|
default = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Edge Server Configuration
|
||||||
|
variable "edge_server_type" {
|
||||||
|
description = "Server type for edge proxy server"
|
||||||
|
type = string
|
||||||
|
default = "cpx22" # 3 vCPU, 4 GB RAM - CPX11/21 unavailable in fsn1
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "edge_location" {
|
||||||
|
description = "Location for edge proxy server"
|
||||||
|
type = string
|
||||||
|
default = "fsn1" # Falkenstein, Germany
|
||||||
|
}
|
||||||
|
|
||||||
# Enable automated snapshots
|
# Enable automated snapshots
|
||||||
variable "enable_snapshots" {
|
variable "enable_snapshots" {
|
||||||
description = "Enable automated daily snapshots (20% of server cost)"
|
description = "Enable automated daily snapshots (20% of server cost)"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue