diff --git a/bdj/deploy.sh b/bdj/deploy.sh index 4311f3c..e8d72ea 100755 --- a/bdj/deploy.sh +++ b/bdj/deploy.sh @@ -1,11 +1,9 @@ #!/usr/bin/env bash -# Build the static site and ensure the edge nginx serves it. -# Content changes need no pod restart (hostPath is live); config changes trigger a reload. +# Build sites and sync docroots. Routing/TLS are handled by Traefik + cert-manager +# in the cluster, so content deploys need no pod reloads or restarts. set -euo pipefail cd "$(dirname "$0")" - BASE=/home/opc/zai-home-base -K="sudo /usr/local/bin/k3s kubectl" echo "==> building site" python3 build.py @@ -13,25 +11,18 @@ python3 build.py mkdir -p "$BASE/sites" rm -rf "$BASE/sites/bestdadjokes.new" cp -r site "$BASE/sites/bestdadjokes.new" -# ensure SELinux-friendly context if SELinux is enforcing if command -v getenforce >/dev/null 2>&1 && [ "$(getenforce)" = "Enforcing" ]; then chcon -Rt container_file_t "$BASE/sites/bestdadjokes.new" 2>/dev/null || true - chcon -Rt container_file_t "$BASE/bdj/nginx/conf.d" 2>/dev/null || true fi rm -rf "$BASE/sites/bestdadjokes.old" [ -d "$BASE/sites/bestdadjokes" ] && mv "$BASE/sites/bestdadjokes" "$BASE/sites/bestdadjokes.old" mv "$BASE/sites/bestdadjokes.new" "$BASE/sites/bestdadjokes" rm -rf "$BASE/sites/bestdadjokes.old" -echo "==> applying edge manifest" -$K apply -f k8s/nginx-edge.yaml - -POD=$($K get pods -l app=nginx-edge -o jsonpath='{.items[0].metadata.name}') -echo "==> reloading nginx config in pod $POD" -$K exec "$POD" -- nginx -s reload 2>/dev/null || true - -echo "==> verifying" +echo "==> verifying through Traefik (origin)" sleep 1 -code=$($K exec "$POD" -- wget -q -O- --header="Host: bestdadjokes.lol" http://127.0.0.1/ | head -c 60) -echo "first bytes of homepage: $code" +for host in bestdadjokes.lol www.bestdadjokes.lol myadhd.dev; do + code=$(curl -sk -o /dev/null -w '%{http_code}' --resolve "$host:443:127.0.0.1" "https://$host/" --max-time 10) + echo " https://$host -> $code (origin)" +done echo "==> deploy complete" diff --git a/bdj/k8s/letsencrypt-issuer.yaml b/bdj/k8s/letsencrypt-issuer.yaml new file mode 100644 index 0000000..9195be8 --- /dev/null +++ b/bdj/k8s/letsencrypt-issuer.yaml @@ -0,0 +1,23 @@ +# Let's Encrypt via cert-manager, DNS-01 through the Cloudflare API token. +# The token lives in Secret cert-manager/cloudflare-api-token (created out-of-band +# via `kubectl create secret ... --from-file=api-token=...`; never committed). +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: letsencrypt-prod +spec: + acme: + server: https://acme-v02.api.letsencrypt.org/directory + email: hello@bestdadjokes.lol + privateKeySecretRef: + name: letsencrypt-prod-account-key + solvers: + - dns01: + cloudflare: + apiTokenSecretRef: + name: cloudflare-api-token + key: api-token + selector: + dnsZones: + - bestdadjokes.lol + - myadhd.dev diff --git a/bdj/k8s/nginx-edge.yaml b/bdj/k8s/nginx-edge.yaml deleted file mode 100644 index 5473cd1..0000000 --- a/bdj/k8s/nginx-edge.yaml +++ /dev/null @@ -1,66 +0,0 @@ -apiVersion: apps/v1 -kind: DaemonSet -metadata: - name: nginx-edge - namespace: default - labels: - app: nginx-edge -spec: - selector: - matchLabels: - app: nginx-edge - template: - metadata: - labels: - app: nginx-edge - spec: - hostNetwork: true - dnsPolicy: ClusterFirstWithHostNet - nodeSelector: - kubernetes.io/hostname: djg-oracle-sl - tolerations: - - key: node-role.kubernetes.io/master - operator: Exists - effect: NoSchedule - - key: node-role.kubernetes.io/control-plane - operator: Exists - effect: NoSchedule - containers: - - name: nginx - image: nginx:alpine - imagePullPolicy: IfNotPresent - ports: - - containerPort: 80 - hostPort: 80 - protocol: TCP - volumeMounts: - - name: conf - mountPath: /etc/nginx/conf.d - readOnly: true - - name: sites - mountPath: /srv/www - readOnly: true - resources: - requests: - cpu: 20m - memory: 32Mi - limits: - memory: 128Mi - livenessProbe: - httpGet: - path: / - port: 80 - httpHeaders: - - name: Host - value: bestdadjokes.lol - initialDelaySeconds: 5 - periodSeconds: 30 - volumes: - - name: conf - hostPath: - path: /home/opc/zai-home-base/bdj/nginx/conf.d - type: Directory - - name: sites - hostPath: - path: /home/opc/zai-home-base/sites - type: DirectoryOrCreate diff --git a/bdj/k8s/static-sites.yaml b/bdj/k8s/static-sites.yaml new file mode 100644 index 0000000..4a5ce32 --- /dev/null +++ b/bdj/k8s/static-sites.yaml @@ -0,0 +1,262 @@ +# Static site fleet: one nginx Deployment + Service + Ingress + Certificate per site. +# Docroots come from hostPath /home/opc/zai-home-base/sites/ (built by deploy scripts). +# Edge routing/TLS: Traefik (80/443) + cert-manager Let's Encrypt secrets. +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: static-nginx-conf +data: + nginx.conf: | + worker_processes 1; + error_log /dev/stderr warn; + pid /run/nginx.pid; + events { worker_connections 1024; } + http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + access_log /dev/stdout; + sendfile on; + gzip on; + gzip_types text/plain text/css application/javascript application/json application/xml image/svg+xml; + gzip_min_length 1024; + server { + listen 80; + root /usr/share/nginx/html; + index index.html; + error_page 404 /404.html; + add_header X-Content-Type-Options nosniff always; + add_header Referrer-Policy strict-origin-when-cross-origin always; + location ~* \.(css|js|svg|png|jpg|ico|woff2?)$ { + expires 7d; + add_header Cache-Control "public"; + } + location / { + try_files $uri $uri/ $uri/index.html =404; + } + } + } +--- +# ---------------- bestdadjokes.lol ---------------- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: static-bdj + labels: + site: bestdadjokes.lol +spec: + replicas: 1 + selector: + matchLabels: + site: bestdadjokes.lol + template: + metadata: + labels: + site: bestdadjokes.lol + spec: + nodeSelector: + kubernetes.io/hostname: djg-oracle-sl + tolerations: + - key: node-role.kubernetes.io/master + operator: Exists + effect: NoSchedule + - key: node-role.kubernetes.io/control-plane + operator: Exists + effect: NoSchedule + containers: + - name: nginx + image: nginx:alpine + volumeMounts: + - name: conf + mountPath: /etc/nginx/nginx.conf + subPath: nginx.conf + readOnly: true + - name: html + mountPath: /usr/share/nginx/html + readOnly: true + resources: + requests: + cpu: 20m + memory: 32Mi + limits: + memory: 128Mi + readinessProbe: + httpGet: {path: /, port: 80} + initialDelaySeconds: 2 + periodSeconds: 20 + volumes: + - name: conf + configMap: + name: static-nginx-conf + - name: html + hostPath: + path: /home/opc/zai-home-base/sites/bestdadjokes + type: Directory +--- +apiVersion: v1 +kind: Service +metadata: + name: static-bdj +spec: + selector: + site: bestdadjokes.lol + ports: + - port: 80 + targetPort: 80 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: bestdadjokes +spec: + rules: + - host: bestdadjokes.lol + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: static-bdj + port: + number: 80 + - host: www.bestdadjokes.lol + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: static-bdj + port: + number: 80 + tls: + - hosts: + - bestdadjokes.lol + - www.bestdadjokes.lol + secretName: bestdadjokes-tls +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: bestdadjokes-lol +spec: + secretName: bestdadjokes-tls + issuerRef: + name: letsencrypt-prod + kind: ClusterIssuer + dnsNames: + - bestdadjokes.lol + - www.bestdadjokes.lol +--- +# ---------------- myadhd.dev (placeholder) ---------------- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: static-adhd + labels: + site: myadhd.dev +spec: + replicas: 1 + selector: + matchLabels: + site: myadhd.dev + template: + metadata: + labels: + site: myadhd.dev + spec: + nodeSelector: + kubernetes.io/hostname: djg-oracle-sl + tolerations: + - key: node-role.kubernetes.io/master + operator: Exists + effect: NoSchedule + - key: node-role.kubernetes.io/control-plane + operator: Exists + effect: NoSchedule + containers: + - name: nginx + image: nginx:alpine + volumeMounts: + - name: conf + mountPath: /etc/nginx/nginx.conf + subPath: nginx.conf + readOnly: true + - name: html + mountPath: /usr/share/nginx/html + readOnly: true + resources: + requests: + cpu: 20m + memory: 32Mi + limits: + memory: 128Mi + readinessProbe: + httpGet: {path: /, port: 80} + initialDelaySeconds: 2 + periodSeconds: 20 + volumes: + - name: conf + configMap: + name: static-nginx-conf + - name: html + hostPath: + path: /home/opc/zai-home-base/sites/myadhd.dev + type: Directory +--- +apiVersion: v1 +kind: Service +metadata: + name: static-adhd +spec: + selector: + site: myadhd.dev + ports: + - port: 80 + targetPort: 80 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: myadhd +spec: + rules: + - host: myadhd.dev + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: static-adhd + port: + number: 80 + - host: www.myadhd.dev + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: static-adhd + port: + number: 80 + tls: + - hosts: + - myadhd.dev + - www.myadhd.dev + secretName: myadhd-tls +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: myadhd-dev +spec: + secretName: myadhd-tls + issuerRef: + name: letsencrypt-prod + kind: ClusterIssuer + dnsNames: + - myadhd.dev + - www.myadhd.dev diff --git a/bdj/k8s/traefik.yaml b/bdj/k8s/traefik.yaml new file mode 100644 index 0000000..0c51f5f --- /dev/null +++ b/bdj/k8s/traefik.yaml @@ -0,0 +1,101 @@ +# Traefik v3 as the cluster edge: hostNetwork 80/443, global http->https redirect. +# TLS certificates are provisioned by cert-manager into Ingress secrets. +apiVersion: v1 +kind: ServiceAccount +metadata: + name: traefik + namespace: kube-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: traefik +rules: + - apiGroups: [""] + resources: ["services", "endpoints", "secrets", "namespaces"] + verbs: ["get", "list", "watch"] + - apiGroups: ["networking.k8s.io"] + resources: ["ingresses", "ingressclasses"] + verbs: ["get", "list", "watch"] + - apiGroups: ["networking.k8s.io"] + resources: ["ingresses/status"] + verbs: ["update"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: traefik +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: traefik +subjects: + - kind: ServiceAccount + name: traefik + namespace: kube-system +--- +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: traefik + namespace: kube-system + labels: + app: traefik +spec: + selector: + matchLabels: + app: traefik + template: + metadata: + labels: + app: traefik + spec: + serviceAccountName: traefik + hostNetwork: true + dnsPolicy: ClusterFirstWithHostNet + nodeSelector: + kubernetes.io/hostname: djg-oracle-sl + tolerations: + - key: node-role.kubernetes.io/master + operator: Exists + effect: NoSchedule + - key: node-role.kubernetes.io/control-plane + operator: Exists + effect: NoSchedule + containers: + - name: traefik + image: traefik:v3.3 + args: + - --providers.kubernetesingress + - --entrypoints.web.address=:80 + - --entrypoints.web.http.redirections.entrypoint.to=websecure + - --entrypoints.web.http.redirections.entrypoint.scheme=https + - --entrypoints.websecure.address=:443 + - --ping=true + - --ping.entrypoint=health + - --entrypoints.health.address=:8082 + ports: + - containerPort: 80 + hostPort: 80 + protocol: TCP + - containerPort: 443 + hostPort: 443 + protocol: TCP + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + memory: 256Mi + readinessProbe: + httpGet: + path: /ping + port: 8082 + initialDelaySeconds: 3 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /ping + port: 8082 + initialDelaySeconds: 10 + periodSeconds: 30 diff --git a/bdj/nginx/README.md b/bdj/nginx/README.md new file mode 100644 index 0000000..3472e61 --- /dev/null +++ b/bdj/nginx/README.md @@ -0,0 +1,2 @@ +# Placeholder: the hand-rolled nginx edge was removed in favor of Traefik + cert-manager. +# Vhosts now live as Ingress resources in bdj/k8s/static-sites.yaml. diff --git a/bdj/nginx/certs/README.md b/bdj/nginx/certs/README.md new file mode 100644 index 0000000..c475c9c --- /dev/null +++ b/bdj/nginx/certs/README.md @@ -0,0 +1,2 @@ +# Placeholder: self-signed origin certs were removed in favor of cert-manager +# issuing Let's Encrypt certificates into k8s Secrets (see bdj/k8s/letsencrypt-issuer.yaml). diff --git a/bdj/nginx/conf.d/bestdadjokes.lol.conf b/bdj/nginx/conf.d/bestdadjokes.lol.conf deleted file mode 100644 index b734505..0000000 --- a/bdj/nginx/conf.d/bestdadjokes.lol.conf +++ /dev/null @@ -1,37 +0,0 @@ -# bestdadjokes.lol -server { - listen 80; - listen [::]:80; - server_name bestdadjokes.lol; - - root /srv/www/bestdadjokes; - index index.html; - - error_page 404 /404.html; - - gzip on; - gzip_types text/plain text/css application/javascript application/json application/xml image/svg+xml; - gzip_min_length 1024; - - add_header X-Content-Type-Options nosniff always; - add_header Referrer-Policy strict-origin-when-cross-origin always; - - location ~* \.(css|js|svg|png|jpg|ico|woff2?)$ { - expires 7d; - add_header Cache-Control "public"; - } - - location = /favicon.ico { return 302 /static/favicon.svg; } - - location / { - try_files $uri $uri/ $uri/index.html =404; - } -} - -# www redirect -server { - listen 80; - listen [::]:80; - server_name www.bestdadjokes.lol; - return 301 http://bestdadjokes.lol$request_uri; -} diff --git a/bdj/nginx/conf.d/default.conf b/bdj/nginx/conf.d/default.conf deleted file mode 100644 index 7c0fcca..0000000 --- a/bdj/nginx/conf.d/default.conf +++ /dev/null @@ -1,7 +0,0 @@ -# catch-all: drop requests for unknown hosts -server { - listen 80 default_server; - listen [::]:80 default_server; - server_name _; - return 444; -}