k8s-native stack: Traefik edge + cert-manager LE DNS-01 + per-site static deployments
This commit is contained in:
parent
0e68abc772
commit
f33385dacc
9 changed files with 397 additions and 126 deletions
|
|
@ -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"
|
||||
|
|
|
|||
23
bdj/k8s/letsencrypt-issuer.yaml
Normal file
23
bdj/k8s/letsencrypt-issuer.yaml
Normal file
|
|
@ -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
|
||||
|
|
@ -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
|
||||
262
bdj/k8s/static-sites.yaml
Normal file
262
bdj/k8s/static-sites.yaml
Normal file
|
|
@ -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/<site> (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
|
||||
101
bdj/k8s/traefik.yaml
Normal file
101
bdj/k8s/traefik.yaml
Normal file
|
|
@ -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
|
||||
2
bdj/nginx/README.md
Normal file
2
bdj/nginx/README.md
Normal file
|
|
@ -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.
|
||||
2
bdj/nginx/certs/README.md
Normal file
2
bdj/nginx/certs/README.md
Normal file
|
|
@ -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).
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
# catch-all: drop requests for unknown hosts
|
||||
server {
|
||||
listen 80 default_server;
|
||||
listen [::]:80 default_server;
|
||||
server_name _;
|
||||
return 444;
|
||||
}
|
||||
Loading…
Reference in a new issue