This commit is contained in:
itumi
2026-02-16 15:37:06 +02:00
parent 70741688e0
commit 6f9d46e833
10 changed files with 983 additions and 937 deletions

60
CLAUDE.md Normal file
View File

@@ -0,0 +1,60 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Kubernetes deployment manifests for "Memelord Jake" — a Django meme-sharing application deployed on a cloud-native Kubernetes cluster. This repo contains **no application source code**, only infrastructure-as-code YAML manifests.
The Django app image is `ghcr.io/l4rm4nd/memelord:latest`. The cluster domain is `ee-lte-1.codemowers.io`.
## Deploying
```bash
kubectl apply -f deployment.yaml # Backing services: PostgreSQL, Redis, S3, networking
kubectl apply -f config.yaml # Django settings.py ConfigMap
kubectl apply -f app.yaml # Memelord Deployment
kubectl apply -f oidc.yaml # OIDC client configuration
kubectl apply -f grafana.yaml # Grafana monitoring (includes namespace)
kubectl apply -f monitoring.yaml # Prometheus Probe
```
The cluster requires these operators pre-installed: CloudNativePG, DragonflyDB, Onyxia S3, cert-manager, Traefik, Codemowers Cloud OIDC, Prometheus operator.
## File Map
- **`deployment.yaml`** — Backing services: StringSecret + Dragonfly (Redis), StringSecret + Cluster + Database (PostgreSQL), Policy + S3User + Bucket (S3), Service + Certificate + Ingress
- **`config.yaml`** — ConfigMap containing the full Django `settings.py`; the largest and most complex file. Configures DB, cache, security headers (CSP/HSTS), storage backends, OIDC, logging
- **`app.yaml`** — Deployment for the Django app (1 replica, port 8000). Mounts `settings.py` from ConfigMap via `subPath`. All config injected via environment variables from Secrets
- **`grafana.yaml`** — Complete Grafana stack: Namespace, ConfigMaps (Prometheus + Loki datasources, dashboard JSON), StatefulSet (5Gi SQLite), OIDC auth, Ingress with TLS
- **`oidc.yaml`** — OIDCClient CR for Memelord app authentication via Passmower
- **`monitoring.yaml`** — Prometheus Probe CR
## Architecture
```
Namespace: memelord-jake
Memelord (Deployment) ──► PostgreSQL (CloudNativePG Cluster)
──► DragonflyDB (Redis-compatible cache/sessions)
──► MinIO S3 (media storage via Onyxia operator)
──► Passmower OIDC (authentication)
Grafana (StatefulSet) ──► Prometheus (monitoring ns)
──► Loki (monitoring ns)
──► Passmower OIDC (authentication)
External access: Traefik Ingress + cert-manager TLS
- memelord-jake.ee-lte-1.codemowers.io
- grafana-jake.ee-lte-1.codemowers.io
```
## Key Conventions
- Resource naming: prefix `memelord-jake-` for all backing services
- Secrets auto-generated via `StringSecret` CRs (mittwald secret generator)
- Django settings are fully environment-driven (12-factor); `config.yaml` reads everything from env vars
- Storage class `postgres` for DB, `sqlite` for Grafana
- Node selector: `codemowers.io/lvm-ubuntu-vg: enterprise-ssd`
- ArgoCD destination cluster: `https://10.254.10.31:6443`
- S3 uses path-style addressing (`AWS_S3_ADDRESSING_STYLE = 'path'`)

3
helm/Chart.yaml Normal file
View File

@@ -0,0 +1,3 @@
apiVersion: v2
name: memelord
version: 0.1.0

View File

@@ -2,20 +2,19 @@
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
name: memelord name: {{ .Release.Name }}
namespace: memelord-jake
spec: spec:
replicas: 1 replicas: 1
selector: selector:
matchLabels: matchLabels:
app: memelord app: {{ .Release.Name }}
template: template:
metadata: metadata:
labels: labels:
app: memelord app: {{ .Release.Name }}
spec: spec:
containers: containers:
- name: memelord - name: {{ .Release.Name }}
image: ghcr.io/l4rm4nd/memelord:latest image: ghcr.io/l4rm4nd/memelord:latest
imagePullPolicy: Always imagePullPolicy: Always
ports: ports:
@@ -24,37 +23,37 @@ spec:
env: env:
- name: DOMAIN - name: DOMAIN
value: "memelord-jake.ee-lte-1.codemowers.io" value: {{ .Values.hostname | quote }}
# Database Configuration # Database Configuration
- name: DB_ENGINE - name: DB_ENGINE
value: "postgres" value: "postgres"
- name: POSTGRES_HOST - name: POSTGRES_HOST
value: "memelord-jake-database-rw" value: "{{ .Release.Name }}-database-rw"
- name: POSTGRES_PORT - name: POSTGRES_PORT
value: "5432" value: "5432"
- name: POSTGRES_DB - name: POSTGRES_DB
value: "memelord-jake" value: {{ .Release.Name | quote }}
- name: POSTGRES_USER - name: POSTGRES_USER
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: memelord-jake-database name: {{ .Release.Name }}-database
key: username key: username
- name: POSTGRES_PASSWORD - name: POSTGRES_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: memelord-jake-database name: {{ .Release.Name }}-database
key: password key: password
# Redis Configuration # Redis Configuration
- name: REDIS_HOST - name: REDIS_HOST
value: "memelord-jake-redis" value: "{{ .Release.Name }}-redis"
- name: REDIS_PORT - name: REDIS_PORT
value: "6379" value: "6379"
- name: REDIS_PASSWORD - name: REDIS_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: memelord-jake-redis name: {{ .Release.Name }}-redis
key: redis-password key: redis-password
# S3/MinIO Storage Configuration # S3/MinIO Storage Configuration
@@ -63,17 +62,17 @@ spec:
- name: AWS_ACCESS_KEY_ID - name: AWS_ACCESS_KEY_ID
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: memelord-jake-bucket name: {{ .Release.Name }}-bucket
key: accessKey key: accessKey
- name: AWS_SECRET_ACCESS_KEY - name: AWS_SECRET_ACCESS_KEY
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: memelord-jake-bucket name: {{ .Release.Name }}-bucket
key: secretKey key: secretKey
- name: AWS_S3_ADDRESSING_STYLE - name: AWS_S3_ADDRESSING_STYLE
value: path value: path
- name: AWS_STORAGE_BUCKET_NAME - name: AWS_STORAGE_BUCKET_NAME
value: "memelord-jake" value: {{ .Release.Name | quote }}
- name: AWS_S3_ENDPOINT_URL - name: AWS_S3_ENDPOINT_URL
value: "https://minio.ee-lte-1.codemowers.io" value: "https://minio.ee-lte-1.codemowers.io"
- name: AWS_S3_REGION_NAME - name: AWS_S3_REGION_NAME
@@ -87,12 +86,12 @@ spec:
- name: OIDC_RP_CLIENT_ID - name: OIDC_RP_CLIENT_ID
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: oidc-client-memelord-jake-owner-secrets name: oidc-client-{{ .Release.Name }}-owner-secrets
key: OIDC_CLIENT_ID key: OIDC_CLIENT_ID
- name: OIDC_RP_CLIENT_SECRET - name: OIDC_RP_CLIENT_SECRET
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: oidc-client-memelord-jake-owner-secrets name: oidc-client-{{ .Release.Name }}-owner-secrets
key: OIDC_CLIENT_SECRET key: OIDC_CLIENT_SECRET
# Browser-facing endpoint (external URL) # Browser-facing endpoint (external URL)

View File

@@ -2,7 +2,6 @@ apiVersion: v1
kind: ConfigMap kind: ConfigMap
metadata: metadata:
name: settings name: settings
namespace: memelord-jake
data: data:
settings.py: | settings.py: |
""" """

View File

@@ -2,7 +2,7 @@
apiVersion: secretgenerator.mittwald.de/v1alpha1 apiVersion: secretgenerator.mittwald.de/v1alpha1
kind: StringSecret kind: StringSecret
metadata: metadata:
name: memelord-jake-redis name: {{ .Release.Name }}-redis
spec: spec:
fields: fields:
- fieldName: redis-password - fieldName: redis-password
@@ -12,11 +12,11 @@ spec:
apiVersion: dragonflydb.io/v1alpha1 apiVersion: dragonflydb.io/v1alpha1
kind: Dragonfly kind: Dragonfly
metadata: metadata:
name: memelord-jake-redis name: {{ .Release.Name }}-redis
spec: spec:
authentication: authentication:
passwordFromSecret: passwordFromSecret:
name: memelord-jake-redis name: {{ .Release.Name }}-redis
key: redis-password key: redis-password
replicas: 1 replicas: 1
resources: resources:
@@ -30,12 +30,12 @@ spec:
apiVersion: secretgenerator.mittwald.de/v1alpha1 apiVersion: secretgenerator.mittwald.de/v1alpha1
kind: StringSecret kind: StringSecret
metadata: metadata:
name: memelord-jake-database name: {{ .Release.Name }}-database
labels: labels:
cnpg.io/reload: "true" cnpg.io/reload: "true"
spec: spec:
data: data:
username: memelord-jake username: {{ .Release.Name }}
fields: fields:
- fieldName: password - fieldName: password
length: "32" length: "32"
@@ -44,7 +44,7 @@ spec:
apiVersion: postgresql.cnpg.io/v1 apiVersion: postgresql.cnpg.io/v1
kind: Cluster kind: Cluster
metadata: metadata:
name: memelord-jake-database name: {{ .Release.Name }}-database
spec: spec:
instances: 1 instances: 1
imageName: ghcr.io/cloudnative-pg/postgresql:17 imageName: ghcr.io/cloudnative-pg/postgresql:17
@@ -69,28 +69,28 @@ spec:
effective_cache_size: "2GB" effective_cache_size: "2GB"
managed: managed:
roles: roles:
- name: memelord-jake - name: {{ .Release.Name }}
ensure: present ensure: present
login: true login: true
passwordSecret: passwordSecret:
name: memelord-jake-database name: {{ .Release.Name }}-database
--- ---
apiVersion: postgresql.cnpg.io/v1 apiVersion: postgresql.cnpg.io/v1
kind: Database kind: Database
metadata: metadata:
name: memelord-jake name: {{ .Release.Name }}
spec: spec:
name: memelord-jake name: {{ .Release.Name }}
owner: memelord-jake owner: {{ .Release.Name }}
cluster: cluster:
name: memelord-jake-database name: {{ .Release.Name }}-database
--- ---
apiVersion: s3.onyxia.sh/v1alpha1 apiVersion: s3.onyxia.sh/v1alpha1
kind: Policy kind: Policy
metadata: metadata:
name: memelord-jake-policy name: {{ .Release.Name }}-policy
spec: spec:
name: memelord-jake-policy name: {{ .Release.Name }}-policy
s3InstanceRef: minio/default s3InstanceRef: minio/default
policyContent: >- policyContent: >-
{ {
@@ -102,8 +102,8 @@ spec:
"s3:*" "s3:*"
], ],
"Resource": [ "Resource": [
"arn:aws:s3:::memelord-jake", "arn:aws:s3:::{{ .Release.Name }}",
"arn:aws:s3:::memelord-jake/*" "arn:aws:s3:::{{ .Release.Name }}/*"
] ]
} }
] ]
@@ -112,19 +112,19 @@ spec:
apiVersion: s3.onyxia.sh/v1alpha1 apiVersion: s3.onyxia.sh/v1alpha1
kind: S3User kind: S3User
metadata: metadata:
name: memelord-jake-bucket name: {{ .Release.Name }}-bucket
spec: spec:
accessKey: memelord-jake-bucket accessKey: {{ .Release.Name }}-bucket
policies: policies:
- memelord-jake-policy - {{ .Release.Name }}-policy
s3InstanceRef: minio/default s3InstanceRef: minio/default
--- ---
apiVersion: s3.onyxia.sh/v1alpha1 apiVersion: s3.onyxia.sh/v1alpha1
kind: Bucket kind: Bucket
metadata: metadata:
name: memelord-jake name: {{ .Release.Name }}
spec: spec:
name: memelord-jake name: {{ .Release.Name }}
s3InstanceRef: minio/default s3InstanceRef: minio/default
quota: quota:
default: 100000000 default: 100000000
@@ -132,11 +132,11 @@ spec:
apiVersion: v1 apiVersion: v1
kind: Service kind: Service
metadata: metadata:
name: memelord name: {{ .Release.Name }}
spec: spec:
type: ClusterIP type: ClusterIP
selector: selector:
app: memelord app: {{ .Release.Name }}
ports: ports:
- name: http - name: http
port: 80 port: 80
@@ -145,11 +145,11 @@ spec:
apiVersion: cert-manager.io/v1 apiVersion: cert-manager.io/v1
kind: Certificate kind: Certificate
metadata: metadata:
name: memelord-jake name: {{ .Release.Name }}
spec: spec:
secretName: memelord-jake-tls secretName: {{ .Release.Name }}-tls
dnsNames: dnsNames:
- memelord-jake.ee-lte-1.codemowers.io - {{ .Values.hostname }}
issuerRef: issuerRef:
name: letsencrypt name: letsencrypt
kind: ClusterIssuer kind: ClusterIssuer
@@ -157,21 +157,21 @@ spec:
apiVersion: networking.k8s.io/v1 apiVersion: networking.k8s.io/v1
kind: Ingress kind: Ingress
metadata: metadata:
name: memelord-jake name: {{ .Release.Name }}
annotations: annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure traefik.ingress.kubernetes.io/router.entrypoints: websecure
spec: spec:
ingressClassName: traefik ingressClassName: traefik
rules: rules:
- host: memelord-jake.ee-lte-1.codemowers.io - host: {{ .Values.hostname }}
http: http:
paths: paths:
- pathType: Prefix - pathType: Prefix
path: "/" path: "/"
backend: backend:
service: service:
name: memelord name: {{ .Release.Name }}
port: port:
number: 80 number: 80
tls: tls:
- secretName: memelord-jake-tls - secretName: {{ .Release.Name }}-tls

View File

@@ -1,13 +1,7 @@
apiVersion: v1 apiVersion: v1
kind: Namespace
metadata:
name: memelord-jake
---
apiVersion: v1
kind: ConfigMap kind: ConfigMap
metadata: metadata:
name: grafana-provisioning name: grafana-provisioning
namespace: memelord-jake
data: data:
datasources.yaml: | datasources.yaml: |
apiVersion: 1 apiVersion: 1
@@ -37,7 +31,6 @@ apiVersion: v1
kind: ConfigMap kind: ConfigMap
metadata: metadata:
name: grafana-dashboards name: grafana-dashboards
namespace: memelord-jake
data: data:
log-aggregator.json: | log-aggregator.json: |
{ {
@@ -155,7 +148,7 @@ data:
"direction": "backward", "direction": "backward",
"editorMode": "code", "editorMode": "code",
"expr": "sum by (detected_level) (count_over_time ({app=~\"$app\",namespace=~\"$namespace\"}[1m]))", "expr": "sum by (detected_level) (count_over_time ({app=~\"$app\",namespace=~\"$namespace\"}[1m]))",
"legendFormat": "{{detected_level}}", "legendFormat": "{{`{{detected_level}}`}}",
"queryType": "range", "queryType": "range",
"refId": "A" "refId": "A"
} }
@@ -287,7 +280,6 @@ apiVersion: apps/v1
kind: StatefulSet kind: StatefulSet
metadata: metadata:
name: grafana name: grafana
namespace: memelord-jake
labels: labels:
app: grafana app: grafana
spec: spec:
@@ -319,7 +311,7 @@ spec:
# Ingress URL (important for OAuth callback + links) # Ingress URL (important for OAuth callback + links)
- name: GF_SERVER_ROOT_URL - name: GF_SERVER_ROOT_URL
value: https://grafana-jake.ee-lte-1.codemowers.io/ value: https://{{ .Values.grafanaHostname }}/
- name: GF_SERVER_SERVE_FROM_SUB_PATH - name: GF_SERVER_SERVE_FROM_SUB_PATH
value: "false" value: "false"
@@ -339,18 +331,15 @@ spec:
- name: GF_AUTH_GENERIC_OAUTH_USE_PKCE - name: GF_AUTH_GENERIC_OAUTH_USE_PKCE
value: "false" value: "false"
# IMPORTANT:
# After OIDCClient grafana-jake is created successfully,
# set this secret name to the generated one (likely oidc-client-grafana-jake-owner-secrets)
- name: GF_AUTH_GENERIC_OAUTH_CLIENT_ID - name: GF_AUTH_GENERIC_OAUTH_CLIENT_ID
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: oidc-client-grafana-jake-owner-secrets name: oidc-client-grafana-{{ .Release.Name }}-owner-secrets
key: OIDC_CLIENT_ID key: OIDC_CLIENT_ID
- name: GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET - name: GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: oidc-client-grafana-jake-owner-secrets name: oidc-client-grafana-{{ .Release.Name }}-owner-secrets
key: OIDC_CLIENT_SECRET key: OIDC_CLIENT_SECRET
- name: GF_AUTH_GENERIC_OAUTH_SCOPES - name: GF_AUTH_GENERIC_OAUTH_SCOPES
@@ -365,7 +354,7 @@ spec:
value: "https://auth.ee-lte-1.codemowers.io/me" value: "https://auth.ee-lte-1.codemowers.io/me"
- name: GF_AUTH_GENERIC_OAUTH_SIGNOUT_REDIRECT_URL - name: GF_AUTH_GENERIC_OAUTH_SIGNOUT_REDIRECT_URL
value: https://grafana-jake.ee-lte-1.codemowers.io/ value: https://{{ .Values.grafanaHostname }}/
volumeMounts: volumeMounts:
- name: grafana-storage - name: grafana-storage
@@ -414,7 +403,6 @@ apiVersion: v1
kind: Service kind: Service
metadata: metadata:
name: grafana name: grafana
namespace: memelord-jake
labels: labels:
app: grafana app: grafana
spec: spec:
@@ -429,12 +417,11 @@ spec:
apiVersion: cert-manager.io/v1 apiVersion: cert-manager.io/v1
kind: Certificate kind: Certificate
metadata: metadata:
name: grafana-jake name: grafana-{{ .Release.Name }}
namespace: memelord-jake
spec: spec:
secretName: grafana-jake-tls secretName: grafana-{{ .Release.Name }}-tls
dnsNames: dnsNames:
- grafana-jake.ee-lte-1.codemowers.io - {{ .Values.grafanaHostname }}
issuerRef: issuerRef:
name: letsencrypt name: letsencrypt
kind: ClusterIssuer kind: ClusterIssuer
@@ -442,14 +429,13 @@ spec:
apiVersion: networking.k8s.io/v1 apiVersion: networking.k8s.io/v1
kind: Ingress kind: Ingress
metadata: metadata:
name: grafana-jake name: grafana-{{ .Release.Name }}
namespace: memelord-jake
annotations: annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure traefik.ingress.kubernetes.io/router.entrypoints: websecure
spec: spec:
ingressClassName: traefik ingressClassName: traefik
rules: rules:
- host: grafana-jake.ee-lte-1.codemowers.io - host: {{ .Values.grafanaHostname }}
http: http:
paths: paths:
- pathType: Prefix - pathType: Prefix
@@ -460,18 +446,17 @@ spec:
port: port:
number: 3000 number: 3000
tls: tls:
- secretName: grafana-jake-tls - secretName: grafana-{{ .Release.Name }}-tls
--- ---
apiVersion: codemowers.cloud/v1beta1 apiVersion: codemowers.cloud/v1beta1
kind: OIDCClient kind: OIDCClient
metadata: metadata:
name: grafana-jake name: grafana-{{ .Release.Name }}
namespace: memelord-jake
spec: spec:
displayName: Grafana jake displayName: Grafana {{ .Release.Name }}
uri: https://grafana-jake.ee-lte-1.codemowers.io/login/generic_oauth uri: https://{{ .Values.grafanaHostname }}/login/generic_oauth
redirectUris: redirectUris:
- https://grafana-jake.ee-lte-1.codemowers.io/login/generic_oauth - https://{{ .Values.grafanaHostname }}/login/generic_oauth
grantTypes: grantTypes:
- authorization_code - authorization_code
- refresh_token - refresh_token

View File

@@ -1,10 +1,9 @@
apiVersion: monitoring.coreos.com/v1 apiVersion: monitoring.coreos.com/v1
kind: Probe kind: Probe
metadata: metadata:
name: memelord-probe name: {{ .Release.Name }}-probe
namespace: memelord-jake
labels: labels:
app: memelord app: {{ .Release.Name }}
spec: spec:
module: http_2xx module: http_2xx
prober: prober:
@@ -12,4 +11,4 @@ spec:
targets: targets:
staticConfig: staticConfig:
static: static:
- google.com - {{ .Values.hostname }}

19
helm/templates/oidc.yaml Normal file
View File

@@ -0,0 +1,19 @@
---
apiVersion: codemowers.cloud/v1beta1
kind: OIDCClient
metadata:
name: {{ .Release.Name }}
spec:
displayName: Memelord {{ .Release.Name }}
uri: https://{{ .Values.hostname }}/
redirectUris:
- https://{{ .Values.hostname }}/oidc/callback/
grantTypes:
- authorization_code
- refresh_token
responseTypes:
- code
availableScopes:
- openid
- profile
pkce: false

2
helm/values.yaml Normal file
View File

@@ -0,0 +1,2 @@
hostname: memelord-jake.ee-lte-1.codemowers.io
grafanaHostname: grafana-jake.ee-lte-1.codemowers.io

View File

@@ -1,20 +0,0 @@
---
apiVersion: codemowers.cloud/v1beta1
kind: OIDCClient
metadata:
name: memelord-jake
namespace: memelord-jake
spec:
displayName: Memelord jake
uri: https://memelord-jake.ee-lte-1.codemowers.io/
redirectUris:
- https://memelord-jake.ee-lte-1.codemowers.io/oidc/callback/
grantTypes:
- authorization_code
- refresh_token
responseTypes:
- code
availableScopes:
- openid
- profile
pkce: false