diff --git a/.gitignore b/.gitignore index 3189e38..e8a8b2c 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,7 @@ vendor/ # Temporary files *.tmp *.temp + +# Kubernetes secrets (never commit these!) +k8s/overlays/*/secrets.yaml +!k8s/overlays/*/secrets.example.yaml diff --git a/README.md b/README.md index 4c6bbcd..58e04b5 100644 --- a/README.md +++ b/README.md @@ -77,9 +77,11 @@ TRUSTED_PROXIES=10.0.0.0/8,172.16.0.0/12 ### Deployment +#### Docker Compose (Recommended for single node) + ```bash # Clone and start -git clone +git clone https://git.dws.rip/DWS/dyn.git cd dyn docker-compose up -d @@ -90,6 +92,44 @@ docker-compose logs -f docker-compose down ``` +#### Kubernetes / K3s + +For production deployments on Kubernetes or K3s clusters: + +```bash +# Clone repository +git clone https://git.dws.rip/DWS/dyn.git +cd dyn + +# Create your secrets file +cp k8s/overlays/production/secrets.example.yaml k8s/overlays/production/secrets.yaml +# Edit secrets.yaml with your actual Technitium credentials + +# Deploy with kustomize +kubectl apply -k k8s/overlays/production + +# Or with kubectl 1.14+ (built-in kustomize) +kubectl apply -k k8s/overlays/production + +# Check deployment status +kubectl get pods -n dyn-ddns +kubectl logs -n dyn-ddns -l app=dyn-ddns + +# Delete deployment +kubectl delete -k k8s/overlays/production +``` + +**Kustomize Overlays:** +- `k8s/overlays/production` - Production setup (2 replicas, higher resource limits) +- `k8s/overlays/staging` - Staging environment (1 replica, relaxed rate limits) + +**Requirements:** +- Cert-manager (for TLS certificates via Let's Encrypt) +- Traefik or NGINX ingress controller +- Persistent storage class (for SQLite database) + +See [k8s/README.md](k8s/README.md) for detailed Kubernetes deployment documentation. + ## API Endpoints ### Web UI diff --git a/k8s/README.md b/k8s/README.md new file mode 100644 index 0000000..109d411 --- /dev/null +++ b/k8s/README.md @@ -0,0 +1,365 @@ +# Kubernetes Deployment + +This directory contains Kubernetes manifests for deploying the DWS Dynamic DNS service on K3s, Kubernetes, or any K8s-compatible platform. + +## Directory Structure + +``` +k8s/ +├── base/ # Base manifests (don't edit directly) +│ ├── deployment.yaml # Deployment, Service, PVC +│ ├── configmap.yaml # Non-sensitive configuration +│ ├── secrets.yaml # Sensitive configuration (placeholders) +│ ├── ingress.yaml # Ingress with TLS +│ └── kustomization.yaml # Base kustomization +│ +├── overlays/ +│ ├── production/ # Production environment +│ │ ├── kustomization.yaml # Production-specific settings +│ │ ├── deployment-patch.yaml # Resource adjustments +│ │ ├── namespace.yaml # Production namespace +│ │ ├── secrets.yaml # Production secrets (gitignored) +│ │ └── secrets.example.yaml # Example secrets template +│ │ +│ └── staging/ # Staging environment +│ ├── kustomization.yaml # Staging-specific settings +│ ├── deployment-patch.yaml # Single replica, lower resources +│ ├── namespace.yaml # Staging namespace +│ ├── secrets.yaml # Staging secrets (gitignored) +│ └── secrets.example.yaml # Example secrets template +│ +└── README.md # This file +``` + +## Quick Start + +### Prerequisites + +- Kubernetes 1.21+ or K3s cluster +- kubectl configured with cluster access +- cert-manager installed (for TLS certificates) +- Ingress controller (Traefik, NGINX, etc.) +- Storage class for persistent volumes + +### Production Deployment + +```bash +# 1. Clone and enter directory +git clone https://git.dws.rip/DWS/dyn.git +cd dyn/k8s + +# 2. Create production secrets +cp overlays/production/secrets.example.yaml overlays/production/secrets.yaml + +# 3. Edit secrets with your Technitium credentials +# Replace 'your-production-api-token-here' with actual token +nano overlays/production/secrets.yaml + +# 4. Deploy to production +kubectl apply -k overlays/production + +# 5. Verify deployment +kubectl get pods -n dyn-ddns +kubectl get svc -n dyn-ddns +kubectl get ingress -n dyn-ddns + +# 6. Check logs +kubectl logs -n dyn-ddns -l app.kubernetes.io/name=dyn-ddns -f +``` + +### Staging Deployment + +```bash +# Similar process for staging +cp overlays/staging/secrets.example.yaml overlays/staging/secrets.yaml +# Edit with staging credentials +kubectl apply -k overlays/staging +``` + +## Configuration + +### Secrets (Required) + +Create `overlays/production/secrets.yaml`: + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: dyn-ddns-secrets +type: Opaque +stringData: + # Choose ONE authentication method: + + # Method 1: API Token (recommended) + TECHNITIUM_TOKEN: "your-actual-api-token-here" + + # Method 2: Username/Password + # TECHNITIUM_USERNAME: "admin" + # TECHNITIUM_PASSWORD: "your-password" + + # Optional: Trusted proxies + # TRUSTED_PROXIES: "10.0.0.0/8,172.16.0.0/12" +``` + +**Important:** Never commit secrets.yaml to git. It's already in .gitignore. + +### ConfigMap (Optional Overrides) + +Edit `overlays/production/kustomization.yaml`: + +```yaml +configMapGenerator: + - name: dyn-ddns-config + behavior: merge + literals: + - TECHNITIUM_URL=https://dns.dws.rip + - BASE_DOMAIN=dws.rip + - SPACE_SUBDOMAIN=space + - RATE_LIMIT_PER_IP=10 + - RATE_LIMIT_PER_TOKEN=1 +``` + +### Ingress Customization + +By default, the ingress is configured for **Traefik** with cert-manager: + +```yaml +annotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure + traefik.ingress.kubernetes.io/router.tls: "true" + cert-manager.io/cluster-issuer: "letsencrypt-prod" +``` + +For **NGINX** ingress, change annotations in `base/ingress.yaml`: + +```yaml +annotations: + kubernetes.io/ingress.class: nginx + cert-manager.io/cluster-issuer: "letsencrypt-prod" + nginx.ingress.kubernetes.io/ssl-redirect: "true" +``` + +## Architecture + +### Components + +1. **Deployment** - Runs the DDNS bridge container + - Replicas: 2 (production), 1 (staging) + - Resource limits configurable per overlay + - Health checks (liveness & readiness probes) + +2. **Service** - ClusterIP exposing port 80 + +3. **Ingress** - TLS termination at edge + - Host: dyn.dws.rip (customize as needed) + - Automatic certificate via cert-manager + +4. **PersistentVolumeClaim** - SQLite database storage + - Size: 1Gi (adjustable) + - AccessMode: ReadWriteOnce + +### Resource Requirements + +**Production:** +- CPU: 200m request / 1000m limit +- Memory: 128Mi request / 512Mi limit +- Replicas: 2 + +**Staging:** +- CPU: 100m request / 500m limit +- Memory: 64Mi request / 256Mi limit +- Replicas: 1 + +## Operations + +### View Logs + +```bash +# All pods +kubectl logs -n dyn-ddns -l app.kubernetes.io/name=dyn-ddns + +# Specific pod +kubectl logs -n dyn-ddns -f deployment/prod-dyn-ddns + +# Previous container logs (after restart) +kubectl logs -n dyn-ddns --previous deployment/prod-dyn-ddns +``` + +### Scale Deployment + +```bash +# Scale to 3 replicas +kubectl scale deployment -n dyn-ddns prod-dyn-ddns --replicas=3 + +# Edit deployment directly +kubectl edit deployment -n dyn-ddns prod-dyn-ddns +``` + +### Database Backup + +The SQLite database is stored in the persistent volume at `/data/dyn.db`: + +```bash +# Find the pod +POD=$(kubectl get pod -n dyn-ddns -l app.kubernetes.io/name=dyn-ddns -o jsonpath='{.items[0].metadata.name}') + +# Copy database locally +kubectl cp dyn-ddns/$POD:/data/dyn.db ./dyn-backup-$(date +%Y%m%d).db + +# Or exec into pod +kubectl exec -it -n dyn-ddns $POD -- sh +# Then: sqlite3 /data/dyn.db "SELECT * FROM spaces;" +``` + +### Update Deployment + +```bash +# Update to latest image +kubectl rollout restart deployment -n dyn-ddns prod-dyn-ddns + +# Or set specific image tag +kubectl set image deployment -n dyn-ddns prod-dyn-ddns dyn-ddns=git.dws.rip/DWS/dyn:v1.0.0 + +# Monitor rollout +kubectl rollout status deployment -n dyn-ddns prod-dyn-ddns +``` + +### Troubleshooting + +**Pod stuck in Pending:** +```bash +kubectl describe pod -n dyn-ddns +# Check: Storage class available? PV provisioned? +``` + +**Pod crash looping:** +```bash +kubectl logs -n dyn-ddns --previous +# Check: Secrets configured? Technitium URL reachable? +``` + +**Ingress not working:** +```bash +kubectl describe ingress -n dyn-ddns prod-dyn-ddns +kubectl get certificate -n dyn-ddns +# Check: DNS pointing to ingress controller? Cert-manager working? +``` + +## Advanced Usage + +### Multi-Environment Setup + +Deploy to multiple environments: + +```bash +# Staging +kubectl apply -k overlays/staging + +# Production +kubectl apply -k overlays/production + +# Verify both +kubectl get pods --all-namespaces -l app.kubernetes.io/name=dyn-ddns +``` + +### Custom Overlay + +Create your own overlay for specific needs: + +```bash +mkdir overlays/custom +cat > overlays/custom/kustomization.yaml << 'EOF' +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - ../../base + +namespace: my-namespace + +configMapGenerator: + - name: dyn-ddns-config + behavior: merge + literals: + - TECHNITIUM_URL=https://my-dns-server.example.com + - RATE_LIMIT_PER_IP=5 +EOF + +kubectl apply -k overlays/custom +``` + +### Monitoring + +The deployment exposes standard metrics. Add Prometheus scraping: + +```yaml +# Add to deployment-patch.yaml +metadata: + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "8080" +``` + +## Security Considerations + +1. **Secrets Management** + - Never commit secrets to git + - Consider using external secrets operator (Vault, Sealed Secrets) + - Rotate Technitium API tokens regularly + +2. **Network Policies** + ```yaml + # Example network policy + apiVersion: networking.k8s.io/v1 + kind: NetworkPolicy + metadata: + name: dyn-ddns-netpol + namespace: dyn-ddns + spec: + podSelector: + matchLabels: + app.kubernetes.io/name: dyn-ddns + policyTypes: + - Ingress + - Egress + ingress: + - from: + - namespaceSelector: + matchLabels: + name: ingress-nginx + ports: + - protocol: TCP + port: 8080 + egress: + - to: [] # Allow all egress (for Technitium API) + ``` + +3. **Pod Security** + - Container runs as non-root (in Dockerfile) + - Read-only root filesystem recommended + - Drop all capabilities + +## Maintenance + +### Regular Tasks + +1. **Backup database weekly** +2. **Monitor rate limit metrics** +3. **Review access logs** +4. **Update base image for security patches** + +### Version Upgrades + +1. Update image tag in overlay kustomization +2. Apply changes: `kubectl apply -k overlays/production` +3. Verify rollout: `kubectl rollout status ...` +4. Monitor for errors in logs + +## Support + +For issues specific to Kubernetes deployment: +1. Check pod logs: `kubectl logs ...` +2. Describe resources: `kubectl describe ...` +3. Check ingress controller logs +4. Verify cert-manager is issuing certificates diff --git a/k8s/base/configmap.yaml b/k8s/base/configmap.yaml new file mode 100644 index 0000000..01b126b --- /dev/null +++ b/k8s/base/configmap.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: dyn-ddns-config +data: + SERVER_PORT: "8080" + DATABASE_PATH: "/data/dyn.db" + BASE_DOMAIN: "dws.rip" + SPACE_SUBDOMAIN: "space" + RATE_LIMIT_PER_IP: "10" + RATE_LIMIT_PER_TOKEN: "1" + # TECHNITIUM_URL: "https://dns.dws.rip" + # Add your Technitium URL above or in overlays diff --git a/k8s/base/deployment.yaml b/k8s/base/deployment.yaml new file mode 100644 index 0000000..adadd87 --- /dev/null +++ b/k8s/base/deployment.yaml @@ -0,0 +1,86 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dyn-ddns + labels: + app: dyn-ddns +spec: + replicas: 1 + selector: + matchLabels: + app: dyn-ddns + template: + metadata: + labels: + app: dyn-ddns + spec: + containers: + - name: dyn-ddns + image: git.dws.rip/DWS/dyn:latest + imagePullPolicy: Always + ports: + - containerPort: 8080 + name: http + envFrom: + - configMapRef: + name: dyn-ddns-config + - secretRef: + name: dyn-ddns-secrets + volumeMounts: + - name: data + mountPath: /data + resources: + requests: + memory: "64Mi" + cpu: "100m" + limits: + memory: "256Mi" + cpu: "500m" + livenessProbe: + httpGet: + path: / + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + httpGet: + path: / + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + volumes: + - name: data + persistentVolumeClaim: + claimName: dyn-ddns-data +--- +apiVersion: v1 +kind: Service +metadata: + name: dyn-ddns + labels: + app: dyn-ddns +spec: + selector: + app: dyn-ddns + ports: + - port: 80 + targetPort: 8080 + name: http + type: ClusterIP +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: dyn-ddns-data + labels: + app: dyn-ddns +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi diff --git a/k8s/base/ingress.yaml b/k8s/base/ingress.yaml new file mode 100644 index 0000000..bc533b1 --- /dev/null +++ b/k8s/base/ingress.yaml @@ -0,0 +1,30 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: dyn-ddns + annotations: + # Traefik + traefik.ingress.kubernetes.io/router.entrypoints: websecure + traefik.ingress.kubernetes.io/router.tls: "true" + cert-manager.io/cluster-issuer: "letsencrypt-prod" + + # NGINX (uncomment if using NGINX ingress) + # kubernetes.io/ingress.class: nginx + # cert-manager.io/cluster-issuer: "letsencrypt-prod" + # nginx.ingress.kubernetes.io/ssl-redirect: "true" +spec: + tls: + - hosts: + - dyn.dws.rip + secretName: dyn-ddns-tls + rules: + - host: dyn.dws.rip + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: dyn-ddns + port: + number: 80 diff --git a/k8s/base/kustomization.yaml b/k8s/base/kustomization.yaml new file mode 100644 index 0000000..78925a0 --- /dev/null +++ b/k8s/base/kustomization.yaml @@ -0,0 +1,17 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - deployment.yaml + - configmap.yaml + - ingress.yaml + +commonLabels: + app.kubernetes.io/name: dyn-ddns + app.kubernetes.io/part-of: dws-dns + +namespace: default + +images: + - name: git.dws.rip/DWS/dyn + newTag: latest diff --git a/k8s/base/secrets.yaml b/k8s/base/secrets.yaml new file mode 100644 index 0000000..11e98a3 --- /dev/null +++ b/k8s/base/secrets.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Secret +metadata: + name: dyn-ddns-secrets +type: Opaque +stringData: + # Choose ONE authentication method: + + # Method 1: API Token (recommended) + # TECHNITIUM_TOKEN: "your-api-token-here" + + # Method 2: Username/Password + # TECHNITIUM_USERNAME: "admin" + # TECHNITIUM_PASSWORD: "your-password" + + # Optional: Trusted proxies (comma-separated) + # TRUSTED_PROXIES: "10.0.0.0/8,172.16.0.0/12" diff --git a/k8s/overlays/production/deployment-patch.yaml b/k8s/overlays/production/deployment-patch.yaml new file mode 100644 index 0000000..a283697 --- /dev/null +++ b/k8s/overlays/production/deployment-patch.yaml @@ -0,0 +1,16 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dyn-ddns +spec: + template: + spec: + containers: + - name: dyn-ddns + resources: + requests: + memory: "128Mi" + cpu: "200m" + limits: + memory: "512Mi" + cpu: "1000m" diff --git a/k8s/overlays/production/kustomization.yaml b/k8s/overlays/production/kustomization.yaml new file mode 100644 index 0000000..76cc320 --- /dev/null +++ b/k8s/overlays/production/kustomization.yaml @@ -0,0 +1,28 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - namespace.yaml + - ../../base + - secrets.yaml + +namePrefix: prod- +namespace: dyn-ddns + +commonLabels: + environment: production + +configMapGenerator: + - name: dyn-ddns-config + behavior: merge + literals: + - TECHNITIUM_URL=https://dns.dws.rip + - RATE_LIMIT_PER_IP=10 + - RATE_LIMIT_PER_TOKEN=1 + +patchesStrategicMerge: + - deployment-patch.yaml + +replicas: + - name: dyn-ddns + count: 2 diff --git a/k8s/overlays/production/namespace.yaml b/k8s/overlays/production/namespace.yaml new file mode 100644 index 0000000..8b8dcbf --- /dev/null +++ b/k8s/overlays/production/namespace.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: dyn-ddns + labels: + app.kubernetes.io/name: dyn-ddns + app.kubernetes.io/part-of: dws-dns diff --git a/k8s/overlays/production/secrets.example.yaml b/k8s/overlays/production/secrets.example.yaml new file mode 100644 index 0000000..dabdd05 --- /dev/null +++ b/k8s/overlays/production/secrets.example.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Secret +metadata: + name: dyn-ddns-secrets +type: Opaque +stringData: + # Replace with your actual Technitium API token + TECHNITIUM_TOKEN: "your-production-api-token-here" + + # Or use username/password (not recommended for production) + # TECHNITIUM_USERNAME: "admin" + # TECHNITIUM_PASSWORD: "your-password" diff --git a/k8s/overlays/staging/deployment-patch.yaml b/k8s/overlays/staging/deployment-patch.yaml new file mode 100644 index 0000000..e62c344 --- /dev/null +++ b/k8s/overlays/staging/deployment-patch.yaml @@ -0,0 +1,17 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dyn-ddns +spec: + replicas: 1 + template: + spec: + containers: + - name: dyn-ddns + resources: + requests: + memory: "64Mi" + cpu: "100m" + limits: + memory: "256Mi" + cpu: "500m" diff --git a/k8s/overlays/staging/kustomization.yaml b/k8s/overlays/staging/kustomization.yaml new file mode 100644 index 0000000..e17ef98 --- /dev/null +++ b/k8s/overlays/staging/kustomization.yaml @@ -0,0 +1,24 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - namespace.yaml + - ../../base + - secrets.yaml + +namePrefix: staging- +namespace: dyn-ddns-staging + +commonLabels: + environment: staging + +configMapGenerator: + - name: dyn-ddns-config + behavior: merge + literals: + - TECHNITIUM_URL=https://dns-staging.dws.rip + - RATE_LIMIT_PER_IP=100 + - RATE_LIMIT_PER_TOKEN=10 + +patchesStrategicMerge: + - deployment-patch.yaml diff --git a/k8s/overlays/staging/namespace.yaml b/k8s/overlays/staging/namespace.yaml new file mode 100644 index 0000000..06b0bcc --- /dev/null +++ b/k8s/overlays/staging/namespace.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: dyn-ddns-staging + labels: + app.kubernetes.io/name: dyn-ddns + app.kubernetes.io/part-of: dws-dns diff --git a/k8s/overlays/staging/secrets.example.yaml b/k8s/overlays/staging/secrets.example.yaml new file mode 100644 index 0000000..9fd768b --- /dev/null +++ b/k8s/overlays/staging/secrets.example.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Secret +metadata: + name: dyn-ddns-secrets +type: Opaque +stringData: + # Replace with your staging Technitium credentials + TECHNITIUM_TOKEN: "your-staging-api-token-here"