refactor(k8s): Reorganize into 5-layer structure with production kustomizations

This commit is contained in:
Story Crater Bot
2026-07-16 14:28:19 -07:00
parent a81b9b6169
commit 2d7330798b
214 changed files with 405 additions and 885 deletions
@@ -0,0 +1,10 @@
# k8s/cert-manager/cert-manager-values.yaml
# cert-manager issues every per-hostname cert off homelab-ca and renews the
# wildcard cert nginx serves — certmanager_certificate_expiration_timestamp_seconds
# is the early-warning signal before any service's TLS breaks.
prometheus:
enabled: true
servicemonitor:
enabled: true
interval: 60s
@@ -0,0 +1,5 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: cert-manager
resources: []
# cert-manager deployed via ArgoCD Helm source (see layer-2-bootstrap app)
+5
View File
@@ -0,0 +1,5 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: kube-system
resources: []
# Cilium deployed via Helm chart
@@ -0,0 +1,39 @@
# k8s/cilium/l2-announcement-policy.yaml
# CiliumL2AnnouncementPolicy — without this, LB-IPAM (lb-ipam-pool.yaml)
# assigns IPs to LoadBalancer Services but nothing ARPs for them on the LAN,
# so they're unreachable from outside the cluster even though `kubectl get
# svc` shows a real EXTERNAL-IP. Confirmed both forgejo's .165 and
# shadowsocks' .166 were 100% packet loss / incomplete ARP before this.
#
# loadBalancerIPs: true makes Cilium announce every Service's LB-IPAM IP via
# ARP from whichever node currently holds the lease for it (one node per IP,
# decided by leaderElection — not all nodes simultaneously, which would
# otherwise cause ARP flapping/duplicate-IP confusion on the LAN).
#
# externalIPs/loadBalancerIPs split exists because Cilium also supports
# announcing Service externalIPs (a different field, unused in this repo);
# we only need loadBalancerIPs since every exposed Service here is type
# LoadBalancer via lb-ipam-pool.yaml.
#
# requires kube-proxy replacement (already the case — see
# k8s/talos-iam or helmfile.yaml.gotmpl kubeProxyReplacement=true) and a
# Cilium build with L2 announcements enabled (default since v1.14).
#
# Apply once after cluster bootstrap, alongside lb-ipam-pool.yaml:
# kubectl apply -f k8s/cilium/l2-announcement-policy.yaml
#
# Verify:
# kubectl get ciliuml2announcementpolicy
# ping 192.168.1.165 && ping 192.168.1.166 # both should now respond
# arp -a | grep 192.168.1.16 # should resolve to a real MAC
apiVersion: cilium.io/v2alpha1
kind: CiliumL2AnnouncementPolicy
metadata:
name: homelab-l2-announce
spec:
loadBalancerIPs: true
interfaces:
- eno1
# No nodeSelector restriction — all 3 nodes already run workloads
# (allowSchedulingOnControlPlanes: true in controlplane.yaml), and with
# 3 zone-labeled nodes, redundancy for per-IP leader election is maintained.
+36
View File
@@ -0,0 +1,36 @@
# k8s/cilium/lb-ipam-pool.yaml
# CiliumLoadBalancerIPPool — tells Cilium LB-IPAM which IPs it can assign
# to LoadBalancer services in this cluster.
#
# CIDR 192.168.1.160/28 covers .160.175 on the LAN:
# .160 talos-cp-1 (node — not assignable to services)
# .161 reserved
# .162 talos-worker-1 (node — not assignable to services)
# .163.175 free for LoadBalancer services
#
# Current service IP assignments (via io.cilium/lb-ipam-ips annotation):
# 192.168.1.165 forgejo-gitea-http (cicd)
# 192.168.1.165 forgejo-gitea-ssh (cicd) — same IP, different ports
# 192.168.1.166 shadowsocks (vpn)
#
# Apply once after cluster bootstrap:
# kubectl apply -f k8s/cilium/lb-ipam-pool.yaml
#
# Verify assignment:
# kubectl get svc -n cicd forgejo-gitea-http forgejo-gitea-ssh
# # EXTERNAL-IP should change from <pending> to 192.168.1.165
apiVersion: "cilium.io/v2alpha1"
kind: CiliumLoadBalancerIPPool
metadata:
name: homelab-pool
spec:
blocks:
- cidr: "192.168.1.160/28"
# DO NOT add any 10.6.0.0/24 block here. That is the WireGuard subnet
# (10.6.0.1 = talos-cp-1 tunnel IP, 10.6.0.2 = DNS — see
# cluster-config/controlplane.yaml). A 10.6.0.x block let Cilium LB-IPAM
# auto-assign the CP's own tunnel IP to a Service, which broke the
# WireGuard tunnel and locked out the default kubectl context. It also
# can't work over WireGuard anyway — L2 announcements only ARP on eno1
# (the LAN interface), not wg0. Keep this pool LAN-only.
@@ -0,0 +1,72 @@
# k8s/coredns/coredns-configmap.yaml
# Patches the CoreDNS Corefile to rewrite homelab hostnames to internal services.
#
# Why this is needed:
# Grafana v10+ does OIDC auto-discovery by fetching
# /.well-known/openid-configuration from Authentik. When Grafana reaches
# Authentik via the external hostname (authentik.riotpiao.homelab.com), the
# HTTP Host header is preserved and Authentik returns external URLs in the
# discovery response. Without this rewrite, the hostname doesn't resolve
# inside the cluster and Grafana falls back to the internal service DNS,
# causing all OAuth redirects to go to authentik-server.iam.svc.cluster.local.
#
# Applied by helmfile presync hook on the ingress-nginx release.
apiVersion: v1
kind: ConfigMap
metadata:
name: coredns
namespace: kube-system
data:
Corefile: |
.:53 {
errors
health {
lameduck 5s
}
ready
log . {
class error
}
prometheus :9153
# Forgejo: route through nginx ingress like every other host below. nginx
# terminates TLS (wildcard-tls) on :443 and routes both /v2/ (container
# registry) and web/git to forgejo-gitea-http:3000.
# Do NOT point this at forgejo-gitea-http directly: that service only serves
# port 3000, so containerd image pulls (which use https/:443) get
# `dial tcp <clusterIP>:443: i/o timeout`. SSH stays on its own LB service.
rewrite name forgejo.riotpiao.homelab.com ingress-nginx-controller.ingress-nginx.svc.cluster.local
# Rewrite homelab hostnames to the nginx ingress controller so in-cluster pods
# hit nginx TLS termination (cert-manager cert) and preserve the Host header.
# Routing through nginx — not directly to the backend service — is critical:
# direct rewrites to the backend bypass nginx TLS and expose each app's own
# self-signed cert, which nothing in the cluster trusts.
rewrite name authentik.riotpiao.homelab.com ingress-nginx-controller.ingress-nginx.svc.cluster.local
rewrite name grafana.riotpiao.homelab.com ingress-nginx-controller.ingress-nginx.svc.cluster.local
rewrite name minio.riotpiao.homelab.com ingress-nginx-controller.ingress-nginx.svc.cluster.local
rewrite name minio-api.riotpiao.homelab.com ingress-nginx-controller.ingress-nginx.svc.cluster.local
rewrite name argocd.riotpiao.homelab.com ingress-nginx-controller.ingress-nginx.svc.cluster.local
rewrite name vault.riotpiao.homelab.com ingress-nginx-controller.ingress-nginx.svc.cluster.local
rewrite name loki.riotpiao.homelab.com ingress-nginx-controller.ingress-nginx.svc.cluster.local
rewrite name prometheus.riotpiao.homelab.com ingress-nginx-controller.ingress-nginx.svc.cluster.local
rewrite name portainer.riotpiao.homelab.com ingress-nginx-controller.ingress-nginx.svc.cluster.local
rewrite name longhorn.riotpiao.homelab.com ingress-nginx-controller.ingress-nginx.svc.cluster.local
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
forward . 8.8.8.8 1.1.1.1 {
max_concurrent 1000
}
cache 30 {
disable success cluster.local
disable denial cluster.local
}
loop
reload
loadbalance
}
@@ -0,0 +1,130 @@
# CoreDNS is deployed by Talos's bootstrap manifests (not a helm release we
# own). This tracks the full Deployment spec as the single source of truth
# for any changes we apply on top of the Talos default — currently just
# topologySpreadConstraints so the 2 replicas don't land on the same node.
apiVersion: apps/v1
kind: Deployment
metadata:
name: coredns
namespace: kube-system
labels:
k8s-app: kube-dns
kubernetes.io/name: CoreDNS
spec:
replicas: 2
revisionHistoryLimit: 10
selector:
matchLabels:
k8s-app: kube-dns
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 1
type: RollingUpdate
template:
metadata:
labels:
k8s-app: kube-dns
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- podAffinityTerm:
labelSelector:
matchExpressions:
- key: k8s-app
operator: In
values:
- kube-dns
topologyKey: kubernetes.io/hostname
weight: 100
containers:
- args:
- -conf
- /etc/coredns/Corefile
env:
- name: GOMEMLIMIT
value: 161MiB
image: registry.k8s.io/coredns/coredns:v1.14.2
imagePullPolicy: IfNotPresent
livenessProbe:
failureThreshold: 5
httpGet:
path: /health
port: 8080
scheme: HTTP
initialDelaySeconds: 60
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 5
name: coredns
ports:
- containerPort: 53
name: dns
protocol: UDP
- containerPort: 53
name: dns-tcp
protocol: TCP
- containerPort: 9153
name: metrics
protocol: TCP
readinessProbe:
failureThreshold: 3
httpGet:
path: /ready
port: 8181
scheme: HTTP
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 1
resources:
limits:
memory: 170Mi
requests:
cpu: 100m
memory: 70Mi
securityContext:
allowPrivilegeEscalation: false
capabilities:
add:
- NET_BIND_SERVICE
drop:
- ALL
readOnlyRootFilesystem: true
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
volumeMounts:
- mountPath: /etc/coredns
name: config-volume
readOnly: true
dnsPolicy: Default
nodeSelector:
kubernetes.io/os: linux
priorityClassName: system-cluster-critical
restartPolicy: Always
schedulerName: default-scheduler
serviceAccount: coredns
serviceAccountName: coredns
terminationGracePeriodSeconds: 30
tolerations:
- effect: NoSchedule
key: node-role.kubernetes.io/control-plane
operator: Exists
- effect: NoSchedule
key: node.cloudprovider.kubernetes.io/uninitialized
operator: Exists
topologySpreadConstraints:
- labelSelector:
matchLabels:
k8s-app: kube-dns
maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
volumes:
- configMap:
defaultMode: 420
items:
- key: Corefile
path: Corefile
name: coredns
name: config-volume
+5
View File
@@ -0,0 +1,5 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: kube-system
resources: []
# CoreDNS deployed via Helm chart
+319
View File
@@ -0,0 +1,319 @@
# k8s/ingress/ingress.yaml
# Ingress rules for all homelab services.
# TLS is handled centrally: nginx serves the wildcard-tls cert (*.riotpiao.homelab.com)
# as its default-ssl-certificate. No per-rule tls: blocks or cert-manager annotations
# are needed — cert-manager manages one cert, nginx uses it for all hosts.
#
# DNS: *.riotpiao.homelab.com must resolve to 10.6.0.1 (WireGuard) or 192.168.1.160 (LAN).
# ── Grafana ───────────────────────────────────────────────────────────────────
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: grafana
namespace: logging
annotations:
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
spec:
ingressClassName: nginx
rules:
- host: grafana.riotpiao.homelab.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: grafana
port:
number: 80
---
# ── Loki (API access for external tools) ─────────────────────────────────────
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: loki
namespace: logging
spec:
ingressClassName: nginx
rules:
- host: loki.riotpiao.homelab.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: loki
port:
number: 3100
---
# ── Authentik ─────────────────────────────────────────────────────────────────
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: authentik
namespace: iam
annotations:
nginx.ingress.kubernetes.io/proxy-buffer-size: "16k"
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
spec:
ingressClassName: nginx
rules:
- host: authentik.riotpiao.homelab.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: authentik-server
port:
number: 80
---
# ── Vault ─────────────────────────────────────────────────────────────────────
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: vault
namespace: iam
annotations:
nginx.ingress.kubernetes.io/backend-protocol: "HTTP"
spec:
ingressClassName: nginx
rules:
- host: vault.riotpiao.homelab.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: vault
port:
number: 8200
---
# ── MinIO console (storage namespace) ────────────────────────────────────────
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: minio
namespace: storage
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "0"
nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
nginx.ingress.kubernetes.io/affinity: "cookie"
nginx.ingress.kubernetes.io/session-cookie-name: "minio-console-affinity"
nginx.ingress.kubernetes.io/session-cookie-max-age: "3600"
spec:
ingressClassName: nginx
rules:
- host: minio.riotpiao.homelab.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: minio-console
port:
number: 9001
---
# ── MinIO S3 API ──────────────────────────────────────────────────────────────
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: minio-api
namespace: storage
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "0"
nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
spec:
ingressClassName: nginx
rules:
- host: minio-api.riotpiao.homelab.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: minio
port:
number: 9000
---
# ── Prometheus ────────────────────────────────────────────────────────────────
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: prometheus
namespace: monitoring
spec:
ingressClassName: nginx
rules:
- host: prometheus.riotpiao.homelab.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: prometheus-kube-prometheus-prometheus
port:
number: 9090
---
# ── Portainer ─────────────────────────────────────────────────────────────────
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: portainer
namespace: dashboard
annotations:
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
spec:
ingressClassName: nginx
rules:
- host: portainer.riotpiao.homelab.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: portainer
port:
number: 9000
---
# ── Forgejo (git forge + OCI registry UI) ────────────────────────────────────
# nginx terminates TLS using the wildcard cert, then proxies plain HTTP to
# Forgejo on port 3000. ROOT_URL stays https:// so Forgejo generates correct URLs.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: forgejo
namespace: cicd
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "0"
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
spec:
ingressClassName: nginx
rules:
- host: forgejo.riotpiao.homelab.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: forgejo-gitea-http
port:
number: 3000
---
# ── Argo CD (cicd namespace) ──────────────────────────────────────────────────
# argocd-server runs HTTPS internally — nginx proxies via backend-protocol: HTTPS.
# proxy-ssl-verify: off because argocd-server's pod cert is self-signed (not homelab-ca).
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: argocd
namespace: cicd
annotations:
nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"
nginx.ingress.kubernetes.io/proxy-ssl-verify: "off"
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
spec:
ingressClassName: nginx
rules:
- host: argocd.riotpiao.homelab.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: argocd-server
port:
number: 443
---
# ── Longhorn UI ───────────────────────────────────────────────────────────────
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: longhorn
namespace: longhorn-system
spec:
ingressClassName: nginx
rules:
- host: longhorn.riotpiao.homelab.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: longhorn-frontend
port:
number: 80
---
# ── Temporal Web UI ────────────────────────────────────────────────────────────
# Temporal workflow orchestration Web UI
# TLS: wildcard cert managed by cert-manager, served by nginx default-ssl-certificate
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: temporal
namespace: temporal
annotations:
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
spec:
ingressClassName: nginx
rules:
- host: temporal.riotpiao.homelab.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: temporal-web
port:
number: 8080
---
# ── kmsvc (SQS/Kafka REST API) ────────────────────────────────────────────
# SQS-compatible messaging API via kmsvc management service
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: kmsvc
namespace: sqs
spec:
ingressClassName: nginx
rules:
- host: kmsvc.riotpiao.homelab.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: management-service
port:
number: 8080
+5
View File
@@ -0,0 +1,5 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: ingress-nginx
resources: []
# Ingress controller deployed via Helm chart
+78
View File
@@ -0,0 +1,78 @@
# k8s/ingress/nginx-values.yaml
# Nginx Ingress Controller — bare-metal homelab config.
# LoadBalancer service with Cilium LB-IPAM assigns fixed IP (192.168.1.160).
# Access services at https://grafana.riotpiao.homelab.com (80/443 via LoadBalancer).
controller:
kind: DaemonSet
# Single wildcard cert served for every *.riotpiao.homelab.com host.
# Applied by the ingress-nginx presync hook (wildcard-cert.yaml) before nginx starts.
# nginx hot-reloads when cert-manager renews homelab-tls — no restart needed.
extraArgs:
default-ssl-certificate: "ingress-nginx/homelab-tls"
hostPort:
enabled: true
ports:
http: 80
https: 443
# TCP proxy: forward port 2222 on every node → Forgejo SSH service.
# This lets `git clone [email protected]:repo` work via the
# same hostname as HTTPS without a separate LoadBalancer IP for SSH.
tcp:
2222: "cicd/forgejo-gitea-ssh:2222"
# Service as LoadBalancer — Cilium LB-IPAM assigns fixed IP.
service:
type: LoadBalancer
annotations:
io.cilium/lb-ipam-ips: "192.168.1.160"
# Allow the controller to land on the control-plane node.
tolerations:
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule
# Use the ingress-nginx IngressClass by default.
ingressClassResource:
default: true
# Required when using hostPort so DNS resolves correctly inside the pod.
dnsPolicy: ClusterFirstWithHostNet
# Reduce noise in a single-admin homelab.
admissionWebhooks:
enabled: false
# ── Connection timeouts to upstreams ──────────────────────────────────────────
# Increased to tolerate 5+ second pod-to-pod network latency spikes.
# Default: 60s for all — acceptable but explicitly set for clarity.
config:
upstream-connect-timeout: "60"
upstream-send-timeout: "60"
upstream-read-timeout: "60"
keepalive-timeout: "65"
keepalive-requests: "100"
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
# RED metrics (rate/errors/duration) for every host fronted by this controller —
# every exposed service in the cluster goes through here, so this single block
# is the cluster-wide "latency and availability" signal. Prometheus auto-discovers
# the ServiceMonitor (serviceMonitorSelectorNilUsesHelmValues: false in prometheus-values.yaml).
metrics:
enabled: true
serviceMonitor:
enabled: true
namespace: ingress-nginx
interval: 30s
scrapeTimeout: 60s
+22
View File
@@ -0,0 +1,22 @@
# k8s/ingress/wildcard-cert.yaml
# Single wildcard TLS certificate for all *.riotpiao.homelab.com services.
# Lives in the ingress-nginx namespace and is set as nginx's default-ssl-certificate,
# so every ingress host gets it automatically — no per-service TLS blocks needed.
#
# Renewal: cert-manager auto-renews 30 days before expiry (renewBefore: 720h).
# nginx detects the secret update via its K8s watch and hot-reloads — no pod restart.
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: homelab-tls
namespace: ingress-nginx
spec:
secretName: homelab-tls
dnsNames:
- "*.riotpiao.homelab.com"
- "riotpiao.homelab.com"
issuerRef:
name: homelab-ca
kind: ClusterIssuer
duration: 8760h # 1 year
renewBefore: 720h # renew 30 days before expiry
+14
View File
@@ -0,0 +1,14 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
metadata:
name: bootstrap
# Layer 2: Bootstrap — networking, certificates, DNS
# Dependencies: infrastructure (namespaces, storage from layer 1)
# Order: Applied second
resources:
- cert-manager/
- cilium/
- coredns/
- ingress/