Files
a13labs.infra/.opencode/plans/gateway-api-migration.md
T
alexandre.pires 8724360fb2 Enhance infrastructure and application configurations
- Updated AGENTS.md to include new directories for execution plans and implemented feature reports.
- Modified Ansible host variables for dev-02 and gpu-01 to change API endpoints and add new configurations for Scaleway.
- Adjusted firewall rules in gpu-01 to allow HTTPS traffic instead of the previous Ollama port.
- Added OAuth2 proxy configurations to gpu-01 for enhanced security.
- Removed deprecated open-webui module from Terraform app configurations.
- Updated Terraform configurations to reflect changes in local variables and removed unused secrets.
- Created a new sectool.json file for CloudNS integration with Bitwarden.
- Enhanced SELinux configurations for nginx to allow connections to any port and added oauth2-proxy port.
2026-07-12 16:28:45 -04:00

14 KiB

Gateway API Migration & Cloudflare Integration

Overview

Migrate prod-01 and dev-01 clusters from the deprecated nginx-ingress controller (retired March 2026) to NGINX Gateway Fabric via the Gateway API, improve cross-cluster WireGuard connectivity, and add Cloudflare Tunnel for edge protection.

Current Architecture

Internet → Contabo Public IP → OPNsense → prod-01
                              ↓
                     nginx-ingress plugin (MicroK8s)
                              ↓
                         MetalLB (10.2.0.40-49)
                              ↓
               ┌───────────────┼─────────────────┐
               ↓               ↓                  ↓
        [proxy pod]      [app pods]        [TCP streams]
      (nginx + WG)  (gitea, m3uproxy, etc)  (hostPort)
          ↓
    WireGuard → aristotle (Contabo VPN)
          ↓
      dev-01 backends (open-webui, nextcloud)

Key facts:

  • 14 kubernetes_ingress_v1 resources across both clusters
  • All use ingress.class: public + nginx-specific annotations
  • Proxy module on prod-01 creates a double-proxy (Ingress → proxy pod → backends)
  • TCP/UDP streams use nginx with hostPorts
  • DNS: ClouDNS, no Cloudflare involvement
  • cert-manager with DNS-01 (dev-01) and HTTP-01 (prod-01) for Let's Encrypt
  • Both clusters are single-node — no need for MetalLB, NodePort is sufficient

Decisions Made

Decision Choice Rationale
Gateway API implementation NGINX Gateway Fabric Keeps nginx data plane, minimal behavioral changes, conformant v1.4.1
Cross-cluster connectivity Improve WireGuard Keep through aristotle, allowed_ips to dev-01 only (10.19.4.136/32), remove gpu-01
Ollama removal Remove gpu-01 runs llamacpp (not ollama), prod-01 ollama proxy is broken, dev-01 DNS CNAME stale
Cloudflare scope Tunnels only Get DDoS+WAF, keep DNS with ClouDNS, least disruptive
Proxy module All through proxy Gateway routes everything to proxy Service, simpler Terraform
Website module Direct Gateway route Keep separate pods, no proxy overhead for static sites
TLS termination At Cloudflare edge Simpler cert management, Cloudflare provides origin cert

Target Architecture

Internet → Cloudflare Tunnel → Cloudflare Edge (TLS termination)
                              ↓
                     cloudflared pods (DaemonSet)
                              ↓
                     Gateway (NGINX Gateway Fabric, NodePort Service)
                     HTTP listener :80 (from cloudflared)
                              ↓
               ┌───────────────┼─────────────────┐
               ↓               ↓                  ↓
        [proxy Service]  [app Services]    [TCP/UDP Routes]
               ↓               ↓                  ↓
      [proxy pod + WG]   [direct pods]      [direct pods]
          (nginx)   (website, etc.)       (rustdesk, etc.)
               ↓
         Cross-cluster → dev-01 backends
         (open-webui, nextcloud)

Phase 1: Gateway API Migration

1A. Install NGINX Gateway Fabric (Ansible)

New file: ansible/roles/microk8s/tasks/gateway-api.yml

- Install `snap` package manager (if needed)
- Install helm via snap
- Add nginx-stable helm repo
- Install Gateway API CRDs (standard channel + experimental for TCPRoute/UDPRoute)
- Install NGINX Gateway Fabric via Helm (experimental features enabled)
- Create GatewayClass named `public`
- Create Gateway Service with type = NodePort (single-node, no MetalLB needed)

Modify: ansible/roles/microk8s/tasks/main.yml

  • Include gateway-api.yml AFTER existing setup (runs alongside nginx-ingress initially)

Modify: ansible/roles/microk8s/defaults/main.yml

  • Add variables: gateway_fabric_version, gateway_fabric_experimental_enabled
  • Remove MetalLB-related vars (microk8s_lb_provider, microk8s_lb_ips, microk8s_metallb_version)

1B. Create Gateway Module (Terraform)

New module: terraform/modules/utils/gateway/

  • main.tf: Gateway resource with HTTP (:80) listener, ServiceType: NodePort
  • variables.tf: TLS secret names, listener config
  • outputs.tf: Gateway service name, NodePort port numbers

Deploy in both clusters (prod-01/apps.tf, dev-01/apps.tf)

1C. Migrate Terraform Modules (Ingress → Gateway API Resources)

Proxy module (terraform/modules/utils/proxy/main.tf)

  • Remove: kubernetes_ingress_v1.ingress resource (lines 160-203)
  • Remove: host_port from stream container ports in Deployment
  • Remove: ollama proxy service definition from prod-01 config.tf
  • Add: HTTPRoute for cross-cluster services (open-webui, nextcloud)
  • Add: TCPRoute resources for RustDesk (21115, 21116, 21117 TCP) and Prometheus (9090)
  • Add: UDPRoute resource for RustDesk (21116 UDP)
  • The proxy pod's nginx stream block can be removed (Gateway handles L4 routing)
  • Keep: WireGuard sidecar, auth sidecar, internal nginx reverse proxy config

Website module (terraform/modules/apps/nginx/main.tf)

  • Remove: kubernetes_ingress_v1.website_ingress (lines 103-145)
  • Add: HTTPRoute per website with backendRef → website Service
  • Gateway handles TLS termination, website pods receive plain HTTP
  • Website pods can drop internal TLS (simplification)

Gitea module (terraform/modules/apps/gitea/ingress.tf)

  • Remove: kubernetes_ingress_v1.gitea_ingress
  • Add: HTTPRoute with backendRef → Gitea Service
  • Use BackendTLSPolicy if Gitea serves HTTPS internally

M3UProxy module (terraform/modules/apps/m3uproxy/main.tf)

  • Remove: kubernetes_ingress_v1.m3uproxy_ingress_http (lines 241-274)
  • Add: HTTPRoute with backendRef → m3uproxy Service

dev-01 modules

Module Current Change
open-webui/ingress.tf kubernetes_ingress_v1 HTTPRoute
grafana/grafana.tf kubernetes_ingress_v1 HTTPRoute
mstream/ingress.tf kubernetes_ingress_v1 HTTPRoute
searxng/ingress.tf kubernetes_ingress_v1 HTTPRoute
vaultwarden/ingress.tf kubernetes_ingress_v1 HTTPRoute
terraform-mcp/ingress.tf kubernetes_ingress_v1 HTTPRoute
nextcloud/ingress.tf kubernetes_ingress_v1 HTTPRoute
proxy/main.tf kubernetes_ingress_v1 HTTPRoute (opencode, vscode)

Annotation → Gateway API Mapping

Current nginx annotation Gateway API equivalent
nginx.org/ssl-redirect: true HTTPRoute with RequestRedirect filter
nginx.org/redirect-to-https: true HTTPRoute on HTTP listener with redirect
nginx.ingress.kubernetes.io/backend-protocol: HTTPS BackendTLSPolicy resource
nginx.ingress.kubernetes.io/proxy-body-size UpstreamSettings policy (NGF custom)
nginx.ingress.kubernetes.io/proxy-request-buffering ProxySettings policy (NGF custom)
nginx.ingress.kubernetes.io/proxy-buffer-size UpstreamSettings policy
cert-manager.io/cluster-issuer cert-manager annotation on Gateway

1D. cert-manager Integration

Use approach B: Keep existing Certificate resources, reference secrets in Gateway TLS config.

  • Each module's kubernetes_manifest Certificate resource stays unchanged
  • Gateway HTTPS listener references cert secrets via certificateRefs
  • This avoids alpha cert-manager Gateway integration

1E. Migration Execution Order

  1. Apply new Ansible role (installs Gateway Fabric alongside nginx-ingress)
  2. Deploy Gateway resource to both clusters
  3. Migrate modules on dev-01 first (lower risk):
    • Deploy HTTPRoutes for each module, one at a time
    • Verify each service works
    • Remove old Ingress resources
  4. Migrate modules on prod-01:
    • Same process, one module at a time
  5. Disable nginx-ingress plugin on both clusters
  6. Clean up IngressClass, old LoadBalancer Service

Phase 2: Cross-Cluster Connectivity

2A. Improve WireGuard

Modify: terraform/apps/prod-01/config.tf (WireGuard config):

  • Change allowed_ips from [10.19.4.106/32, 10.19.4.136/32]["10.19.4.136/32"] (dev-01 only)
  • Remove gpu-01 (10.19.4.106) - ollama proxy being removed, no need to reach gpu-01
  • persistent_keepalive already set to 25

Modify: terraform/modules/utils/proxy/resources/wireguard/wireguard.conf.tpl:

  • Template PersistentKeepalive from config variable
  • Template AllowedIPs from config list

Also clean up:

  • Remove "ollama" entry from proxy_services in prod-01 config.tf
  • Remove ollama CNAME from dev-01 DNS zones in dev-01 config.tf

Modify: terraform/modules/utils/proxy/main.tf (WireGuard container):

  • Improve liveness probe: wg show instead of wg show | grep -q transfer
  • Add readiness probe
  • Add resource limits (memory cap)

2B. DNS Resolution

No changes needed. The proxy's per-location resolver in default.conf.tpl already handles DNS correctly for cross-cluster backends.


Phase 3: Cloudflare Tunnels

3A. Cloudflare Setup

  • Create/verify Cloudflare account with alexpires.me domain
  • Keep DNS with ClouDNS (Cloudflare acts as reverse proxy only)
  • Create Cloudflare Tunnel for prod-01
  • Configure route rules: https://*.alexpires.me → Gateway Service
  • Set up origin rules (TLS terminates at Cloudflare edge → HTTP to cloudflared)

3B. Deploy cloudflared

New module: terraform/modules/utils/cloudflare-tunnel/

  • DaemonSet of cloudflare/cloudflared pods
  • ConfigMap with tunnel credentials and route definitions
  • Each public FQDN mapped to http://<gateway-service>.<namespace>:80

3C. Gateway Configuration Changes

  • Gateway uses HTTP (:80) NodePort listener for Cloudflare Tunnel traffic
  • Cloudflare Tunnel connects to http://<node_ip>:<nodeport>
  • No HTTPS listener needed since all traffic comes through Cloudflare Tunnel

3D. Firewall Changes

  • Close UFW ports 80/443 (cloudflared uses ephemeral outbound ports)
  • Keep SSH + WireGuard ports open

3E. Security Benefits

With Cloudflare in front:

  • No open ports on prod-01 (80, 443, etc. closed)
  • DDoS protection (automatic with Cloudflare)
  • WAF rules (OWASP Top 10, custom rules)
  • Rate limiting on public endpoints
  • Bot management

Phase 4: Cleanup

  • Remove nginx-ingress plugin from MicroK8s (microk8s.disable ingress)
  • Remove unused IngressClass resources
  • Remove existing MetalLB (delete metallb-system namespace, IPAddressPool, L2Advertisement)
  • Delete orphaned tasks/loadbalancer.yml and tasks/loadbalancer/metallb.yaml
  • Remove MetalLB vars from defaults/main.yml
  • Update documentation

File Change Summary

New Files

File Purpose
ansible/roles/microk8s/tasks/gateway-api.yml NGINX Gateway Fabric install
terraform/modules/utils/gateway/main.tf Gateway + ReferenceGrant resources
terraform/modules/utils/gateway/variables.tf Gateway configuration
terraform/modules/utils/gateway/outputs.tf Gateway outputs
terraform/modules/utils/cloudflare-tunnel/main.tf cloudflared DaemonSet
terraform/modules/utils/cloudflare-tunnel/variables.tf Tunnel config

Deleted Files

File Reason
ansible/roles/microk8s/tasks/loadbalancer.yml Not wired into main.yml, was for nginx-ingress LB
ansible/roles/microk8s/tasks/loadbalancer/metallb.yaml MetalLB no longer needed (single-node, NodePort)

Modified Files

File Change
ansible/roles/microk8s/tasks/main.yml Include gateway-api.yml
ansible/roles/microk8s/defaults/main.yml Add Gateway Fabric defaults, remove MetalLB vars
terraform/modules/utils/proxy/main.tf Ingress → HTTPRoute + TCPRoute + UDPRoute
terraform/modules/utils/proxy/variables.tf Update for Gateway API
terraform/modules/utils/proxy/resources/wireguard/wireguard.conf.tpl Add PersistentKeepalive
terraform/modules/apps/nginx/main.tf Ingress → HTTPRoute
terraform/modules/apps/gitea/ingress.tf Ingress → HTTPRoute
terraform/modules/apps/m3uproxy/main.tf Ingress → HTTPRoute
terraform/modules/apps/open-webui/ingress.tf Ingress → HTTPRoute
terraform/modules/apps/grafana/grafana.tf Ingress → HTTPRoute
terraform/modules/apps/mstream/ingress.tf Ingress → HTTPRoute
terraform/modules/apps/searxng/ingress.tf Ingress → HTTPRoute
terraform/modules/apps/vaultwarden/ingress.tf Ingress → HTTPRoute
terraform/modules/apps/terraform-mcp/ingress.tf Ingress → HTTPRoute
terraform/modules/apps/nextcloud/ingress.tf Ingress → HTTPRoute
terraform/modules/apps/ssh_locker_web/ Ingress → HTTPRoute
terraform/apps/prod-01/apps.tf Add gateway module
terraform/apps/prod-01/config.tf WireGuard (remove gpu-01), remove ollama proxy
terraform/apps/dev-01/config.tf Remove ollama DNS CNAME
terraform/apps/dev-01/apps.tf Add gateway module

Risk Assessment

Risk Mitigation
TCPRoute/UDPRoute experimental in NGF Start with HTTPRoute only, keep proxy streams temporarily
Cloudflare Tunnel adds latency Monitor, acceptable for reverse proxy traffic
cert-manager Gateway integration alpha Use approach B (separate Certificate resources)
Downtime during migration Run both controllers in parallel, switch via DNS TTL
Removing gpu-01 from WireGuard allowed_ips Verify no active proxy depends on gpu-01 reachability
Removing existing MetalLB on prod-01 Do after Cloudflare Tunnel is fully operational

Timeline Estimate

Phase Duration Dependencies
Phase 1 (Gateway API) 2-3 weeks None
Phase 2 (WireGuard) 1-2 weeks Can run parallel with Phase 1
Phase 3 (Cloudflare) 1-2 weeks After Phase 1
Phase 4 (Cleanup) 1 week After Phase 3