Adding traefik to the mix
Homelab Main / deploy (push) Successful in 1m55s
Details
Homelab Main / deploy (push) Successful in 1m55s
Details
This commit is contained in:
parent
ccb8247577
commit
ad81d37119
98
README.md
98
README.md
|
|
@ -48,8 +48,8 @@ accidentally modify the cluster.
|
||||||
3. `bootstrap/platform`
|
3. `bootstrap/platform`
|
||||||
- installs a minimal Calico deployment through the Tigera operator
|
- installs a minimal Calico deployment through the Tigera operator
|
||||||
- installs NodeLocal DNSCache for node-local DNS query caching
|
- installs NodeLocal DNSCache for node-local DNS query caching
|
||||||
- can install MetalLB for LAN `LoadBalancer` services after an address pool
|
- installs MetalLB for LAN `LoadBalancer` services
|
||||||
is chosen
|
- installs Traefik as the single Kubernetes ingress entry point
|
||||||
- installs OpenEBS
|
- installs OpenEBS
|
||||||
- creates `openebs-hostpath-retain`
|
- creates `openebs-hostpath-retain`
|
||||||
- installs Argo CD
|
- installs Argo CD
|
||||||
|
|
@ -245,29 +245,58 @@ you intentionally accept losing that monitoring data. A planned monitoring data
|
||||||
migration should be handled as a separate maintenance task with backup,
|
migration should be handled as a separate maintenance task with backup,
|
||||||
delete/recreate or storage migration steps, and post-restore checks.
|
delete/recreate or storage migration steps, and post-restore checks.
|
||||||
|
|
||||||
The website and demos NodePorts are reachable from the OCI jump box through the
|
The older NodePort path is now reserved for special cases such as the local
|
||||||
Raspberry Pi Tailscale interface. `bootstrap/cluster` installs a persistent
|
registry. `bootstrap/cluster` still contains `homelab-tailscale-nodeport`
|
||||||
`homelab-tailscale-nodeport.service` on the configured worker to restore the
|
support, but app traffic should normally enter through Traefik's MetalLB
|
||||||
route, rp_filter settings, and iptables rules after reboot. Override the
|
address instead of per-app NodePorts. The cluster stack advertises the LAN
|
||||||
defaults through `tailscale_nodeport_access` when the jump-box IP, Pi Tailscale
|
subnet from the configured Tailscale worker so the OCI edge can route to the
|
||||||
IP, pod CIDR, primary NodePort, or pod target port changes. Add any additional
|
Traefik LoadBalancer address:
|
||||||
public NodePorts to `tailscale_nodeport_extra_ports`:
|
|
||||||
|
|
||||||
```hcl
|
```hcl
|
||||||
tailscale_nodeport_access = {
|
metallb = {
|
||||||
enabled = true
|
enabled = true
|
||||||
worker_key = "raspberrypi"
|
repository = "https://metallb.github.io/metallb"
|
||||||
peer_ip = "100.118.255.19"
|
version = "0.16.0"
|
||||||
node_tailscale_ip = "100.77.80.72"
|
namespace = "metallb-system"
|
||||||
pod_cidr = "10.244.0.0/16"
|
address_pool = ["192.168.100.240-192.168.100.240"]
|
||||||
node_port = 30080
|
l2_advertisement_enabled = true
|
||||||
target_port = 8080
|
pool_name = "homelab-lan"
|
||||||
}
|
}
|
||||||
|
|
||||||
tailscale_nodeport_extra_ports = [30081, 30082, 30083, 30084, 30085, 30086]
|
traefik = {
|
||||||
tailscale_nodeport_extra_target_ports = [80, 3000, 9090, 9093]
|
enabled = true
|
||||||
|
repository = "https://helm.traefik.io/traefik"
|
||||||
|
chart_version = "40.2.0"
|
||||||
|
namespace = "traefik"
|
||||||
|
load_balancer_ip = "192.168.100.240"
|
||||||
|
ingress_class = "traefik"
|
||||||
|
}
|
||||||
|
|
||||||
|
tailscale_subnet_routes = {
|
||||||
|
enabled = true
|
||||||
|
worker_key = "raspberrypi"
|
||||||
|
routes = ["192.168.100.0/24"]
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
DuckDNS resolves `*.lab2025.duckdns.org` to the OCI edge, so public requests for
|
||||||
|
the service hostnames land on the same edge host. For direct LAN testing, point
|
||||||
|
LAN DNS, `/etc/hosts`, or a Tailscale DNS override for app hostnames at
|
||||||
|
Traefik's address:
|
||||||
|
|
||||||
|
```text
|
||||||
|
192.168.100.240 lab2025.duckdns.org
|
||||||
|
192.168.100.240 demos.lab2025.duckdns.org
|
||||||
|
192.168.100.240 heimdall.lab2025.duckdns.org
|
||||||
|
192.168.100.240 grafana.lab2025.duckdns.org
|
||||||
|
192.168.100.240 argocd.lab2025.duckdns.org
|
||||||
|
```
|
||||||
|
|
||||||
|
The edge stack uses Traefik as its backend by default and validates
|
||||||
|
`http://192.168.100.240:80/` before updating the edge containers. If Tailscale
|
||||||
|
subnet-route approval is not automatic in the tailnet policy, the edge deploy
|
||||||
|
will fail clearly instead of silently keeping a broken public path.
|
||||||
|
|
||||||
For `./lab.sh nuke`, set `WORKER_SSH_TARGETS` to a space-separated list of
|
For `./lab.sh nuke`, set `WORKER_SSH_TARGETS` to a space-separated list of
|
||||||
remote SSH targets when more worker nodes exist. Set it to an empty string for a
|
remote SSH targets when more worker nodes exist. Set it to an empty string for a
|
||||||
single-node rebuild.
|
single-node rebuild.
|
||||||
|
|
@ -311,9 +340,11 @@ export TF_VAR_metallb='{
|
||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
The current website, demos, and registry services remain `NodePort` services
|
Traefik uses MetalLB for a LAN `LoadBalancer` address. App services such as the
|
||||||
until the LAN address pool and edge route are tested manually. Gitea is not a
|
website, demos, and Heimdall should be `ClusterIP` services behind Kubernetes
|
||||||
Kubernetes service; it runs on the Raspberry Pi Docker host.
|
Ingress objects. The local registry remains a `NodePort` because the cluster
|
||||||
|
nodes use it as a pull endpoint. Gitea is not a Kubernetes service; it runs on
|
||||||
|
the Raspberry Pi Docker host.
|
||||||
|
|
||||||
## Secrets
|
## Secrets
|
||||||
|
|
||||||
|
|
@ -327,28 +358,29 @@ outside the repo. Operational notes are in `docs/secrets.md`.
|
||||||
The OCI jump box runs the public edge path:
|
The OCI jump box runs the public edge path:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
nginx -> HAProxy -> Varnish/Squid -> Raspberry Pi Tailscale NodePort
|
nginx -> HAProxy -> Varnish/Squid -> Traefik MetalLB IP
|
||||||
```
|
```
|
||||||
|
|
||||||
The `bootstrap/edge` stack renders configs from `bootstrap/edge/templates` and
|
The `bootstrap/edge` stack renders configs from `bootstrap/edge/templates` and
|
||||||
deploys them to `/opt/homelab-edge` on the OCI host. Defaults are in
|
deploys them to `/opt/homelab-edge` on the OCI host. Defaults are in
|
||||||
`bootstrap/edge/variables.tf`; override them through `TF_VAR_*` or a `.tfvars`
|
`bootstrap/edge/variables.tf`; override them through `TF_VAR_*` or a `.tfvars`
|
||||||
file when the public host, SSH key, server name, backend Tailscale IP, or
|
file when the public host, SSH key, server name, backend Tailscale IP, or
|
||||||
NodePort changes.
|
Traefik backend address changes.
|
||||||
|
|
||||||
The `/git/` route is intentionally different from the Kubernetes app routes: it
|
The `/git/` route is intentionally different from the Kubernetes app routes: it
|
||||||
proxies to Gitea on the Raspberry Pi at the configured `backend_host` and
|
proxies to Gitea on the Raspberry Pi at the configured `gitea_backend_port`
|
||||||
`gitea_backend_port` instead of a Kubernetes NodePort. This keeps public
|
instead of Traefik. This keeps public read-only source browsing available even
|
||||||
read-only source browsing available even when the cluster has been destroyed.
|
when the cluster has been destroyed.
|
||||||
|
|
||||||
Use the configured `server_name` in the browser, for example
|
Use the configured `server_name` in the browser, for example
|
||||||
`https://lab2025.duckdns.org`. A raw OCI IP address will still show a browser
|
`https://lab2025.duckdns.org`. A raw OCI IP address will still show a browser
|
||||||
certificate warning because the trusted certificate is issued for the hostname.
|
certificate warning because the trusted certificate is issued for the hostname.
|
||||||
|
|
||||||
The edge stack uses HTTP-01 validation, so public DNS for `server_name` must
|
The edge stack uses HTTP-01 validation and requests one certificate covering
|
||||||
point to the OCI public IP and inbound TCP 80 and 443 must be open before
|
`server_name` plus `additional_server_names`. DuckDNS resolves sub-subdomains
|
||||||
`./lab.sh up` runs. Set `TF_VAR_letsencrypt_email` to receive expiry notices,
|
under `lab2025.duckdns.org` to the same edge IP, so inbound TCP 80 and 443 must
|
||||||
or leave it empty to register without an email. Set
|
be open before `./lab.sh up` runs. Set `TF_VAR_letsencrypt_email` to receive
|
||||||
|
expiry notices, or leave it empty to register without an email. Set
|
||||||
`TF_VAR_enable_letsencrypt=false` to keep using the temporary local certificate.
|
`TF_VAR_enable_letsencrypt=false` to keep using the temporary local certificate.
|
||||||
|
|
||||||
## Adding Apps
|
## Adding Apps
|
||||||
|
|
@ -362,8 +394,8 @@ It runs the LinuxServer.io Heimdall dashboard, persists `/config` on
|
||||||
OpenEBS, and seeds tiles for the website, demo apps, Gitea, Grafana,
|
OpenEBS, and seeds tiles for the website, demo apps, Gitea, Grafana,
|
||||||
Prometheus, Alertmanager, Argo CD, the local registry, and Heimdall itself.
|
Prometheus, Alertmanager, Argo CD, the local registry, and Heimdall itself.
|
||||||
Because Heimdall does not support reverse-proxy subfolder hosting cleanly, it
|
Because Heimdall does not support reverse-proxy subfolder hosting cleanly, it
|
||||||
is exposed through NodePort `30082`; the dashboard's internal tool links use
|
is exposed through the dedicated hostname `heimdall.lab2025.duckdns.org` rather
|
||||||
the Raspberry Pi Tailscale NodePort address.
|
than a `/heimdall/` path.
|
||||||
|
|
||||||
## Storage
|
## Storage
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
resources:
|
||||||
|
- namespace.yaml
|
||||||
|
- web-app.yaml
|
||||||
|
|
@ -75,11 +75,27 @@ metadata:
|
||||||
name: demos-static-service
|
name: demos-static-service
|
||||||
namespace: demos-static
|
namespace: demos-static
|
||||||
spec:
|
spec:
|
||||||
type: NodePort
|
|
||||||
externalTrafficPolicy: Local
|
|
||||||
ports:
|
ports:
|
||||||
- port: 80
|
- port: 80
|
||||||
targetPort: http
|
targetPort: http
|
||||||
nodePort: 30081
|
|
||||||
selector:
|
selector:
|
||||||
app: demos-static
|
app: demos-static
|
||||||
|
---
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: demos-static
|
||||||
|
namespace: demos-static
|
||||||
|
spec:
|
||||||
|
ingressClassName: traefik
|
||||||
|
rules:
|
||||||
|
- host: demos.lab2025.duckdns.org
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: demos-static-service
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: heimdall
|
||||||
|
namespace: heimdall
|
||||||
|
spec:
|
||||||
|
ingressClassName: traefik
|
||||||
|
rules:
|
||||||
|
- host: heimdall.lab2025.duckdns.org
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: heimdall
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
|
---
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: grafana
|
||||||
|
namespace: monitoring
|
||||||
|
spec:
|
||||||
|
ingressClassName: traefik
|
||||||
|
rules:
|
||||||
|
- host: grafana.lab2025.duckdns.org
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: prometheus-stack-grafana
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
|
---
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: prometheus
|
||||||
|
namespace: monitoring
|
||||||
|
spec:
|
||||||
|
ingressClassName: traefik
|
||||||
|
rules:
|
||||||
|
- host: prometheus.lab2025.duckdns.org
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: prometheus-stack-kube-prom-prometheus
|
||||||
|
port:
|
||||||
|
number: 9090
|
||||||
|
---
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: alertmanager
|
||||||
|
namespace: monitoring
|
||||||
|
spec:
|
||||||
|
ingressClassName: traefik
|
||||||
|
rules:
|
||||||
|
- host: alertmanager.lab2025.duckdns.org
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: prometheus-stack-kube-prom-alertmanager
|
||||||
|
port:
|
||||||
|
number: 9093
|
||||||
|
---
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: argocd-server
|
||||||
|
namespace: argocd
|
||||||
|
annotations:
|
||||||
|
traefik.ingress.kubernetes.io/service.serversscheme: https
|
||||||
|
spec:
|
||||||
|
ingressClassName: traefik
|
||||||
|
rules:
|
||||||
|
- host: argocd.lab2025.duckdns.org
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: argocd-server
|
||||||
|
port:
|
||||||
|
number: 443
|
||||||
|
|
@ -3,4 +3,4 @@ kind: Kustomization
|
||||||
resources:
|
resources:
|
||||||
- namespace.yaml
|
- namespace.yaml
|
||||||
- web-app.yaml
|
- web-app.yaml
|
||||||
- ui-nodeports.yaml
|
- ingress.yaml
|
||||||
|
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: grafana-nodeport
|
|
||||||
namespace: monitoring
|
|
||||||
spec:
|
|
||||||
type: NodePort
|
|
||||||
ports:
|
|
||||||
- name: http
|
|
||||||
port: 80
|
|
||||||
targetPort: 3000
|
|
||||||
nodePort: 30083
|
|
||||||
selector:
|
|
||||||
app.kubernetes.io/instance: prometheus-stack
|
|
||||||
app.kubernetes.io/name: grafana
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: prometheus-nodeport
|
|
||||||
namespace: monitoring
|
|
||||||
spec:
|
|
||||||
type: NodePort
|
|
||||||
ports:
|
|
||||||
- name: http
|
|
||||||
port: 9090
|
|
||||||
targetPort: 9090
|
|
||||||
nodePort: 30084
|
|
||||||
selector:
|
|
||||||
app.kubernetes.io/name: prometheus
|
|
||||||
operator.prometheus.io/name: prometheus-stack-kube-prom-prometheus
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: alertmanager-nodeport
|
|
||||||
namespace: monitoring
|
|
||||||
spec:
|
|
||||||
type: NodePort
|
|
||||||
ports:
|
|
||||||
- name: http
|
|
||||||
port: 9093
|
|
||||||
targetPort: 9093
|
|
||||||
nodePort: 30085
|
|
||||||
selector:
|
|
||||||
app.kubernetes.io/name: alertmanager
|
|
||||||
alertmanager: prometheus-stack-kube-prom-alertmanager
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: argocd-server-nodeport
|
|
||||||
namespace: argocd
|
|
||||||
spec:
|
|
||||||
type: NodePort
|
|
||||||
ports:
|
|
||||||
- name: https
|
|
||||||
port: 443
|
|
||||||
targetPort: 8080
|
|
||||||
nodePort: 30086
|
|
||||||
selector:
|
|
||||||
app.kubernetes.io/name: argocd-server
|
|
||||||
|
|
@ -28,7 +28,7 @@ data:
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Demo Apps",
|
"title": "Demo Apps",
|
||||||
"url": "https://lab2025.duckdns.org/demo-apps/",
|
"url": "http://demos.lab2025.duckdns.org/",
|
||||||
"description": "Static browser-side demo catalog",
|
"description": "Static browser-side demo catalog",
|
||||||
"colour": "#2563eb",
|
"colour": "#2563eb",
|
||||||
"class": "Demos"
|
"class": "Demos"
|
||||||
|
|
@ -42,28 +42,28 @@ data:
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Grafana",
|
"title": "Grafana",
|
||||||
"url": "http://100.77.80.72:30083/",
|
"url": "http://grafana.lab2025.duckdns.org/",
|
||||||
"description": "Monitoring dashboards",
|
"description": "Monitoring dashboards",
|
||||||
"colour": "#f97316",
|
"colour": "#f97316",
|
||||||
"class": "Grafana"
|
"class": "Grafana"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Prometheus",
|
"title": "Prometheus",
|
||||||
"url": "http://100.77.80.72:30084/",
|
"url": "http://prometheus.lab2025.duckdns.org/",
|
||||||
"description": "Prometheus query UI",
|
"description": "Prometheus query UI",
|
||||||
"colour": "#dc2626",
|
"colour": "#dc2626",
|
||||||
"class": "Prometheus"
|
"class": "Prometheus"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Alertmanager",
|
"title": "Alertmanager",
|
||||||
"url": "http://100.77.80.72:30085/",
|
"url": "http://alertmanager.lab2025.duckdns.org/",
|
||||||
"description": "Alert routing and silences",
|
"description": "Alert routing and silences",
|
||||||
"colour": "#b91c1c",
|
"colour": "#b91c1c",
|
||||||
"class": "Alertmanager"
|
"class": "Alertmanager"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Argo CD",
|
"title": "Argo CD",
|
||||||
"url": "https://100.77.80.72:30086/",
|
"url": "http://argocd.lab2025.duckdns.org/",
|
||||||
"description": "GitOps application sync status",
|
"description": "GitOps application sync status",
|
||||||
"colour": "#0ea5e9",
|
"colour": "#0ea5e9",
|
||||||
"class": "ArgoCD"
|
"class": "ArgoCD"
|
||||||
|
|
@ -77,7 +77,7 @@ data:
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Heimdall",
|
"title": "Heimdall",
|
||||||
"url": "http://100.77.80.72:30082/",
|
"url": "http://heimdall.lab2025.duckdns.org/",
|
||||||
"description": "Homelab service dashboard",
|
"description": "Homelab service dashboard",
|
||||||
"colour": "#7c3aed",
|
"colour": "#7c3aed",
|
||||||
"class": "Heimdall"
|
"class": "Heimdall"
|
||||||
|
|
@ -282,10 +282,8 @@ metadata:
|
||||||
name: heimdall
|
name: heimdall
|
||||||
namespace: heimdall
|
namespace: heimdall
|
||||||
spec:
|
spec:
|
||||||
type: NodePort
|
|
||||||
ports:
|
ports:
|
||||||
- port: 80
|
- port: 80
|
||||||
targetPort: http
|
targetPort: http
|
||||||
nodePort: 30082
|
|
||||||
selector:
|
selector:
|
||||||
app: heimdall
|
app: heimdall
|
||||||
|
|
|
||||||
|
|
@ -119,11 +119,27 @@ metadata:
|
||||||
name: php-website-service
|
name: php-website-service
|
||||||
namespace: website-production
|
namespace: website-production
|
||||||
spec:
|
spec:
|
||||||
type: NodePort
|
|
||||||
externalTrafficPolicy: Local
|
|
||||||
ports:
|
ports:
|
||||||
- port: 80
|
- port: 80
|
||||||
targetPort: http
|
targetPort: http
|
||||||
nodePort: 30080
|
|
||||||
selector:
|
selector:
|
||||||
app: php-website
|
app: php-website
|
||||||
|
---
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: php-website
|
||||||
|
namespace: website-production
|
||||||
|
spec:
|
||||||
|
ingressClassName: traefik
|
||||||
|
rules:
|
||||||
|
- host: lab2025.duckdns.org
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: php-website-service
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
|
|
|
||||||
|
|
@ -352,6 +352,9 @@ resource "null_resource" "kubeadm_worker" {
|
||||||
tailscale_nodeport_pod_cidr = var.tailscale_nodeport_access.pod_cidr
|
tailscale_nodeport_pod_cidr = var.tailscale_nodeport_access.pod_cidr
|
||||||
tailscale_nodeport_node_ports = join(" ", distinct(concat([var.tailscale_nodeport_access.node_port], var.tailscale_nodeport_extra_ports)))
|
tailscale_nodeport_node_ports = join(" ", distinct(concat([var.tailscale_nodeport_access.node_port], var.tailscale_nodeport_extra_ports)))
|
||||||
tailscale_nodeport_target_ports = join(" ", distinct(concat([var.tailscale_nodeport_access.target_port], var.tailscale_nodeport_extra_target_ports)))
|
tailscale_nodeport_target_ports = join(" ", distinct(concat([var.tailscale_nodeport_access.target_port], var.tailscale_nodeport_extra_target_ports)))
|
||||||
|
tailscale_subnet_routes_version = "1"
|
||||||
|
tailscale_subnet_routes_enabled = var.tailscale_subnet_routes.enabled && each.key == var.tailscale_subnet_routes.worker_key ? "true" : "false"
|
||||||
|
tailscale_subnet_routes = join(",", var.tailscale_subnet_routes.routes)
|
||||||
}
|
}
|
||||||
|
|
||||||
connection {
|
connection {
|
||||||
|
|
@ -695,6 +698,31 @@ configure_tailscale_nodeport_access \
|
||||||
"${self.triggers.tailscale_nodeport_node_ports}" \
|
"${self.triggers.tailscale_nodeport_node_ports}" \
|
||||||
"${self.triggers.tailscale_nodeport_target_ports}"
|
"${self.triggers.tailscale_nodeport_target_ports}"
|
||||||
|
|
||||||
|
configure_tailscale_subnet_routes() {
|
||||||
|
local enabled="$1"
|
||||||
|
local routes="$2"
|
||||||
|
|
||||||
|
if [ "$enabled" != "true" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v tailscale >/dev/null 2>&1; then
|
||||||
|
echo "tailscale is required to advertise subnet routes but is not installed on this worker." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$routes" ]; then
|
||||||
|
echo "tailscale subnet route advertisement is enabled but no routes were configured." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
sudo tailscale set --advertise-routes="$routes"
|
||||||
|
}
|
||||||
|
|
||||||
|
configure_tailscale_subnet_routes \
|
||||||
|
"${self.triggers.tailscale_subnet_routes_enabled}" \
|
||||||
|
"${self.triggers.tailscale_subnet_routes}"
|
||||||
|
|
||||||
configure_containerd_registry "${self.triggers.registry_endpoint}"
|
configure_containerd_registry "${self.triggers.registry_endpoint}"
|
||||||
|
|
||||||
pv_dirs="${self.triggers.persistent_volume_dirs}"
|
pv_dirs="${self.triggers.persistent_volume_dirs}"
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ variable "tailscale_nodeport_access" {
|
||||||
})
|
})
|
||||||
|
|
||||||
default = {
|
default = {
|
||||||
enabled = true
|
enabled = false
|
||||||
worker_key = "raspberrypi"
|
worker_key = "raspberrypi"
|
||||||
peer_ip = "100.118.255.19"
|
peer_ip = "100.118.255.19"
|
||||||
node_tailscale_ip = "100.77.80.72"
|
node_tailscale_ip = "100.77.80.72"
|
||||||
|
|
@ -93,10 +93,24 @@ variable "tailscale_nodeport_access" {
|
||||||
|
|
||||||
variable "tailscale_nodeport_extra_ports" {
|
variable "tailscale_nodeport_extra_ports" {
|
||||||
type = list(number)
|
type = list(number)
|
||||||
default = [30081, 30082, 30083, 30084, 30085, 30086]
|
default = []
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "tailscale_nodeport_extra_target_ports" {
|
variable "tailscale_nodeport_extra_target_ports" {
|
||||||
type = list(number)
|
type = list(number)
|
||||||
default = [80, 3000, 9090, 9093]
|
default = []
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "tailscale_subnet_routes" {
|
||||||
|
type = object({
|
||||||
|
enabled = bool
|
||||||
|
worker_key = string
|
||||||
|
routes = list(string)
|
||||||
|
})
|
||||||
|
|
||||||
|
default = {
|
||||||
|
enabled = true
|
||||||
|
worker_key = "raspberrypi"
|
||||||
|
routes = ["192.168.100.0/24"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,10 @@ terraform {
|
||||||
|
|
||||||
locals {
|
locals {
|
||||||
compose_file = templatefile("${path.module}/templates/docker-compose.yml.tftpl", {})
|
compose_file = templatefile("${path.module}/templates/docker-compose.yml.tftpl", {})
|
||||||
|
server_names = distinct(concat([var.server_name], var.additional_server_names))
|
||||||
default_conf = templatefile("${path.module}/templates/default.conf.tftpl", {
|
default_conf = templatefile("${path.module}/templates/default.conf.tftpl", {
|
||||||
server_name = var.server_name
|
server_name = var.server_name
|
||||||
|
server_names = join(" ", local.server_names)
|
||||||
backend_host = var.backend_host
|
backend_host = var.backend_host
|
||||||
demos_backend_port = var.demos_backend_port
|
demos_backend_port = var.demos_backend_port
|
||||||
gitea_backend_port = var.gitea_backend_port
|
gitea_backend_port = var.gitea_backend_port
|
||||||
|
|
@ -43,6 +45,10 @@ resource "null_resource" "edge_services" {
|
||||||
user = var.edge_user
|
user = var.edge_user
|
||||||
install_dir = var.edge_install_dir
|
install_dir = var.edge_install_dir
|
||||||
server_name = var.server_name
|
server_name = var.server_name
|
||||||
|
server_names = join(" ", local.server_names)
|
||||||
|
certbot_domain_args = join(" ", [for name in local.server_names : "-d ${name}"])
|
||||||
|
backend_host = var.backend_host
|
||||||
|
backend_port = tostring(var.backend_port)
|
||||||
enable_letsencrypt = tostring(var.enable_letsencrypt)
|
enable_letsencrypt = tostring(var.enable_letsencrypt)
|
||||||
letsencrypt_email = var.letsencrypt_email
|
letsencrypt_email = var.letsencrypt_email
|
||||||
letsencrypt_staging = tostring(var.letsencrypt_staging)
|
letsencrypt_staging = tostring(var.letsencrypt_staging)
|
||||||
|
|
@ -96,6 +102,10 @@ set -eu
|
||||||
|
|
||||||
install_dir="${self.triggers.install_dir}"
|
install_dir="${self.triggers.install_dir}"
|
||||||
server_name="${self.triggers.server_name}"
|
server_name="${self.triggers.server_name}"
|
||||||
|
server_names="${self.triggers.server_names}"
|
||||||
|
certbot_domain_args="${self.triggers.certbot_domain_args}"
|
||||||
|
backend_host="${self.triggers.backend_host}"
|
||||||
|
backend_port="${self.triggers.backend_port}"
|
||||||
enable_letsencrypt="${self.triggers.enable_letsencrypt}"
|
enable_letsencrypt="${self.triggers.enable_letsencrypt}"
|
||||||
letsencrypt_email="${self.triggers.letsencrypt_email}"
|
letsencrypt_email="${self.triggers.letsencrypt_email}"
|
||||||
letsencrypt_staging="${self.triggers.letsencrypt_staging}"
|
letsencrypt_staging="${self.triggers.letsencrypt_staging}"
|
||||||
|
|
@ -116,6 +126,12 @@ install_missing_packages() {
|
||||||
|
|
||||||
install_missing_packages ca-certificates curl openssl
|
install_missing_packages ca-certificates curl openssl
|
||||||
|
|
||||||
|
if ! curl -fsS --connect-timeout 10 -H "Host: $server_name" "http://$backend_host:$backend_port/" >/dev/null; then
|
||||||
|
echo "Cannot reach Traefik backend http://$backend_host:$backend_port/ for $server_name from the edge host." >&2
|
||||||
|
echo "Check that MetalLB assigned the Traefik LoadBalancer IP and that the edge host has a route to it." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
if ! command -v docker >/dev/null 2>&1; then
|
if ! command -v docker >/dev/null 2>&1; then
|
||||||
curl -fsSL https://get.docker.com | sudo sh
|
curl -fsSL https://get.docker.com | sudo sh
|
||||||
fi
|
fi
|
||||||
|
|
@ -243,7 +259,7 @@ if [ "$enable_letsencrypt" = "true" ]; then
|
||||||
"$certbot_image" certonly \
|
"$certbot_image" certonly \
|
||||||
--webroot \
|
--webroot \
|
||||||
-w /var/www/certbot \
|
-w /var/www/certbot \
|
||||||
-d "$server_name" \
|
$certbot_domain_args \
|
||||||
--preferred-challenges http \
|
--preferred-challenges http \
|
||||||
--agree-tos \
|
--agree-tos \
|
||||||
--non-interactive \
|
--non-interactive \
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ real_ip_header CF-Connecting-IP;
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name ${server_name};
|
server_name ${server_names};
|
||||||
|
|
||||||
location ^~ /.well-known/acme-challenge/ {
|
location ^~ /.well-known/acme-challenge/ {
|
||||||
root /var/www/certbot;
|
root /var/www/certbot;
|
||||||
|
|
@ -46,7 +46,7 @@ server {
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 443 ssl;
|
listen 443 ssl;
|
||||||
server_name ${server_name};
|
server_name ${server_names};
|
||||||
|
|
||||||
ssl_certificate /etc/nginx/certs/current.crt;
|
ssl_certificate /etc/nginx/certs/current.crt;
|
||||||
ssl_certificate_key /etc/nginx/certs/current.key;
|
ssl_certificate_key /etc/nginx/certs/current.key;
|
||||||
|
|
@ -97,19 +97,7 @@ server {
|
||||||
}
|
}
|
||||||
|
|
||||||
location ^~ /demo-apps/ {
|
location ^~ /demo-apps/ {
|
||||||
limit_req zone=one burst=20 nodelay;
|
return 301 https://demos.${server_name}/;
|
||||||
|
|
||||||
proxy_pass http://${backend_host}:${demos_backend_port}/;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto https;
|
|
||||||
proxy_set_header CF-Connecting-IP $http_cf_connecting_ip;
|
|
||||||
|
|
||||||
proxy_cache static_assets;
|
|
||||||
proxy_cache_valid 200 301 302 15m;
|
|
||||||
proxy_cache_key "$scheme$request_method$host$request_uri";
|
|
||||||
add_header X-Nginx-Cache "$upstream_cache_status";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
location ~* \.(css|js)$ {
|
location ~* \.(css|js)$ {
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,18 @@ variable "server_name" {
|
||||||
default = "lab2025.duckdns.org"
|
default = "lab2025.duckdns.org"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
variable "additional_server_names" {
|
||||||
|
type = list(string)
|
||||||
|
default = [
|
||||||
|
"demos.lab2025.duckdns.org",
|
||||||
|
"heimdall.lab2025.duckdns.org",
|
||||||
|
"grafana.lab2025.duckdns.org",
|
||||||
|
"prometheus.lab2025.duckdns.org",
|
||||||
|
"alertmanager.lab2025.duckdns.org",
|
||||||
|
"argocd.lab2025.duckdns.org",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
variable "enable_letsencrypt" {
|
variable "enable_letsencrypt" {
|
||||||
type = bool
|
type = bool
|
||||||
default = true
|
default = true
|
||||||
|
|
@ -40,12 +52,12 @@ variable "letsencrypt_staging" {
|
||||||
|
|
||||||
variable "backend_host" {
|
variable "backend_host" {
|
||||||
type = string
|
type = string
|
||||||
default = "100.77.80.72"
|
default = "192.168.100.240"
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "backend_port" {
|
variable "backend_port" {
|
||||||
type = number
|
type = number
|
||||||
default = 30080
|
default = 80
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "demos_backend_port" {
|
variable "demos_backend_port" {
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,10 @@ EOT
|
||||||
"kubernetes.io/os" = "linux"
|
"kubernetes.io/os" = "linux"
|
||||||
"homelab.dev/workload-class" = "platform"
|
"homelab.dev/workload-class" = "platform"
|
||||||
}
|
}
|
||||||
|
traefik_node_selector = {
|
||||||
|
"kubernetes.io/os" = "linux"
|
||||||
|
"homelab.dev/workload-class" = "platform"
|
||||||
|
}
|
||||||
|
|
||||||
argocd_component_label_values = {
|
argocd_component_label_values = {
|
||||||
application_set = "argocd-applicationset-controller"
|
application_set = "argocd-applicationset-controller"
|
||||||
|
|
@ -753,6 +757,85 @@ EOT
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resource "helm_release" "traefik" {
|
||||||
|
for_each = var.traefik.enabled ? { enabled = true } : {}
|
||||||
|
|
||||||
|
depends_on = [null_resource.metallb_l2_config]
|
||||||
|
name = "traefik"
|
||||||
|
repository = var.traefik.repository
|
||||||
|
chart = "traefik"
|
||||||
|
version = var.traefik.chart_version
|
||||||
|
namespace = var.traefik.namespace
|
||||||
|
create_namespace = true
|
||||||
|
timeout = 600
|
||||||
|
wait = true
|
||||||
|
|
||||||
|
values = [
|
||||||
|
yamlencode({
|
||||||
|
deployment = {
|
||||||
|
replicas = 1
|
||||||
|
}
|
||||||
|
nodeSelector = local.traefik_node_selector
|
||||||
|
ingressClass = {
|
||||||
|
enabled = true
|
||||||
|
isDefaultClass = true
|
||||||
|
name = var.traefik.ingress_class
|
||||||
|
}
|
||||||
|
gateway = {
|
||||||
|
enabled = false
|
||||||
|
}
|
||||||
|
gatewayClass = {
|
||||||
|
enabled = false
|
||||||
|
}
|
||||||
|
providers = {
|
||||||
|
kubernetesCRD = {
|
||||||
|
enabled = true
|
||||||
|
}
|
||||||
|
kubernetesIngress = {
|
||||||
|
enabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ports = {
|
||||||
|
web = {
|
||||||
|
port = 80
|
||||||
|
exposedPort = 80
|
||||||
|
}
|
||||||
|
websecure = {
|
||||||
|
port = 443
|
||||||
|
exposedPort = 443
|
||||||
|
tls = {
|
||||||
|
enabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
service = {
|
||||||
|
type = "LoadBalancer"
|
||||||
|
annotations = {
|
||||||
|
"metallb.universe.tf/address-pool" = var.metallb.pool_name
|
||||||
|
}
|
||||||
|
spec = {
|
||||||
|
externalTrafficPolicy = "Local"
|
||||||
|
loadBalancerIP = var.traefik.load_balancer_ip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logs = {
|
||||||
|
general = {
|
||||||
|
level = "INFO"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resources = {
|
||||||
|
requests = {
|
||||||
|
cpu = "50m"
|
||||||
|
memory = "96Mi"
|
||||||
|
}
|
||||||
|
limits = {
|
||||||
|
memory = "256Mi"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
resource "helm_release" "openebs" {
|
resource "helm_release" "openebs" {
|
||||||
depends_on = [null_resource.calico_ready]
|
depends_on = [null_resource.calico_ready]
|
||||||
name = "openebs"
|
name = "openebs"
|
||||||
|
|
|
||||||
|
|
@ -124,16 +124,36 @@ variable "metallb" {
|
||||||
})
|
})
|
||||||
|
|
||||||
default = {
|
default = {
|
||||||
enabled = false
|
enabled = true
|
||||||
repository = "https://metallb.github.io/metallb"
|
repository = "https://metallb.github.io/metallb"
|
||||||
version = "0.16.0"
|
version = "0.16.0"
|
||||||
namespace = "metallb-system"
|
namespace = "metallb-system"
|
||||||
address_pool = []
|
address_pool = ["192.168.100.240-192.168.100.240"]
|
||||||
l2_advertisement_enabled = true
|
l2_advertisement_enabled = true
|
||||||
pool_name = "homelab-lan"
|
pool_name = "homelab-lan"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
variable "traefik" {
|
||||||
|
type = object({
|
||||||
|
enabled = bool
|
||||||
|
repository = string
|
||||||
|
chart_version = string
|
||||||
|
namespace = string
|
||||||
|
load_balancer_ip = string
|
||||||
|
ingress_class = string
|
||||||
|
})
|
||||||
|
|
||||||
|
default = {
|
||||||
|
enabled = true
|
||||||
|
repository = "https://helm.traefik.io/traefik"
|
||||||
|
chart_version = "40.2.0"
|
||||||
|
namespace = "traefik"
|
||||||
|
load_balancer_ip = "192.168.100.240"
|
||||||
|
ingress_class = "traefik"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
variable "observability" {
|
variable "observability" {
|
||||||
type = object({
|
type = object({
|
||||||
namespace = string
|
namespace = string
|
||||||
|
|
|
||||||
2
lab.sh
2
lab.sh
|
|
@ -122,6 +122,8 @@ adopt_platform_existing_resources() {
|
||||||
adopt_tofu_helm_release "${stack}" "helm_release.calico_crds" "tigera-operator" "calico-crds"
|
adopt_tofu_helm_release "${stack}" "helm_release.calico_crds" "tigera-operator" "calico-crds"
|
||||||
adopt_tofu_helm_release "${stack}" "helm_release.calico" "tigera-operator" "calico"
|
adopt_tofu_helm_release "${stack}" "helm_release.calico" "tigera-operator" "calico"
|
||||||
adopt_tofu_helm_release "${stack}" "helm_release.openebs" "openebs" "openebs"
|
adopt_tofu_helm_release "${stack}" "helm_release.openebs" "openebs" "openebs"
|
||||||
|
adopt_tofu_helm_release "${stack}" "helm_release.metallb[\"enabled\"]" "metallb-system" "metallb"
|
||||||
|
adopt_tofu_helm_release "${stack}" "helm_release.traefik[\"enabled\"]" "traefik" "traefik"
|
||||||
adopt_tofu_helm_release "${stack}" "helm_release.argocd" "argocd" "argocd"
|
adopt_tofu_helm_release "${stack}" "helm_release.argocd" "argocd" "argocd"
|
||||||
adopt_tofu_helm_release "${stack}" "helm_release.kyverno" "kyverno" "kyverno"
|
adopt_tofu_helm_release "${stack}" "helm_release.kyverno" "kyverno" "kyverno"
|
||||||
adopt_tofu_helm_release "${stack}" "helm_release.kyverno_policies" "kyverno" "kyverno-policies"
|
adopt_tofu_helm_release "${stack}" "helm_release.kyverno_policies" "kyverno" "kyverno-policies"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue