Hybrid cloud cluster monorepo with Ansible, K8s, NixOS, and Terraform. cute.haus
ansible terraform nix k8s
1

Configure Feed

Select the types of activity you want to include in your feed.

k8s: rm common chart

Aly Raffauf (Jun 25, 2026, 1:55 AM EDT) cf432b4c 86bb1adf

+149 -346
+109 -86
k8s/charts/README.md
··· 1 - # 🪐 Charts 1 + # Charts 2 2 3 3 In-tree Helm charts deployed by [`../helmfile.yaml`](../helmfile.yaml). 4 4 5 - App charts share a library chart (`common/`); their `templates/*.yaml` files 6 - are one-line includes of `common.deployment` / `common.service` / etc., and 7 - all per-app config lives in `values.yaml`. 5 + Most app charts use explicit Kubernetes manifests. Helm is used for light 6 + substitution, mostly `.Chart.Name` and secret values passed by helmfile. Avoid 7 + shared Deployment/Service/PVC helpers; app-specific behavior should stay visible 8 + in the app chart. 8 9 9 - --- 10 + ## Layout 10 11 11 - ## 📂 Layout 12 - 13 - ```plaintext 12 + ```text 14 13 charts/ 15 14 ├── aly-codes/ # Static site (aly.codes) 15 + ├── audiobookshelf/ # Audiobook library with rclone-mounted media 16 16 ├── bluesky-pds/ # Reference atproto Personal Data Server 17 - ├── cert-manager-issuers/ # Let's Encrypt ClusterIssuer + wildcard Certificates (Cloudflare DNS-01) 17 + ├── cert-manager-issuers/ # Let's Encrypt ClusterIssuer + wildcard Certificates 18 18 ├── cluster-tls/ # Cloudflare Origin TLS Secrets per domain 19 - ├── common/ # Library chart with shared partials 20 19 ├── external-routes/ # Ingress + Service + EndpointSlice for off-cluster targets 21 20 ├── forgejo/ # Git hosting (git.aly.codes) 22 - ├── longhorn-creds/ # B2 backup-target Secret + recurring backup job + UI ingress 23 - ├── morsels/ # atproto pastebin (morsels.blue) 21 + ├── forward-auth/ # Per-app traefik-forward-auth frontends 22 + ├── immich/ # Photo library + ML + app-specific Postgres 23 + ├── longhorn-creds/ # B2 backup Secret + recurring backup job + UI ingress 24 + ├── paperless/ # Document management with rclone media mount 24 25 ├── pg-shared/ # CloudNativePG cluster using local-path replicas 25 - ├── tranquil-pds/ # TRanquil atproto Personal Data Server 26 - ├── uptime-kuma/ # Uptime monitoring + status pages 27 - ├── vaultwarden/ # Bitwarden-compatible vault 28 - └── watsup/ # Homelab dashboard (cute.haus) 26 + └── ... 29 27 ``` 30 28 31 - --- 29 + ## Chart Style 32 30 33 - ## 🔐 Secret flow 31 + Prefer direct manifests: 34 32 33 + ```yaml 34 + apiVersion: apps/v1 35 + kind: Deployment 36 + metadata: 37 + name: { { .Chart.Name } } 38 + labels: 39 + app: { { .Chart.Name } } 40 + spec: 41 + selector: 42 + matchLabels: 43 + app: { { .Chart.Name } } 44 + template: 45 + metadata: 46 + labels: 47 + app: { { .Chart.Name } } 48 + spec: 49 + automountServiceAccountToken: false 50 + enableServiceLinks: false 51 + containers: 52 + - name: { { .Chart.Name } } 53 + image: example/app:1.0.0@sha256:... 35 54 ``` 36 - secrets/foo.yaml (SOPS-encrypted, multi-recipient age) 37 - 38 - ▼ vals reads ref+sops:// at render time 39 - values/foo.yaml (plain yaml of ref+sops://... refs) 40 - 41 - ▼ helmfile passes it as values: to the release 42 - chart's values.yaml (.Values.secret.* now plaintext during render) 43 - 44 - 45 - common.secret partial → kind: Secret with stringData 46 - 47 - 48 - deployment.envFrom → env vars in the container 55 + 56 + Use this pod-spec order when practical: 57 + 58 + ```yaml 59 + automountServiceAccountToken: false 60 + enableServiceLinks: false 61 + terminationGracePeriodSeconds: 30 62 + securityContext: 63 + tolerations: 64 + hostNetwork: 65 + dnsPolicy: 66 + nodeSelector: 67 + imagePullSecrets: 68 + topologySpreadConstraints: 69 + initContainers: 70 + containers: 71 + volumes: 49 72 ``` 50 73 51 - `vals` resolves `ref+sops://` URLs at render time using whichever age key 52 - is at `~/.config/sops/age/keys.txt` (run `just sops-bootstrap` once on a 53 - new machine to derive that from the SSH host key). 74 + Only include fields that the app actually needs. Do not add shared helpers just 75 + to make every chart identical. 54 76 55 - --- 77 + ## Templating Rules 78 + 79 + Keep these: 80 + 81 + - `.Chart.Name` for names, labels, selectors, and PVC names. 82 + - `if .Values.secret` around Secret manifests populated by vals/SOPS. 83 + - Small `range` loops for charts that are naturally data-driven. 56 84 57 - ## 🆕 Add a new app 85 + Avoid these: 58 86 59 - ```bash 60 - just new-app <name> 61 - ``` 87 + - Common Deployment/Service/PVC helper templates. 88 + - Values that model arbitrary pod specs, containers, sidecars, volumes, or env. 89 + - Large `_helpers.tpl` files for simple app charts. 62 90 63 - Then: 91 + ## Data-Driven Exceptions 64 92 65 - 1. Edit `charts/<name>/values.yaml` — image, ports, env, ingress routes, 66 - persistence. 67 - 2. Add a release block to `helmfile.yaml`: 68 - ```yaml 69 - - name: <name> 70 - namespace: default 71 - chart: ./charts/<name> 72 - ``` 73 - 3. If the app needs secrets: 74 - - `just sops-edit <name>.yaml` — write the encrypted file 75 - - Create `values/<name>.yaml` with `ref+sops://../secrets/<name>.yaml#/...` 76 - refs for each key (path is relative to `k8s/`, where helmfile runs) 77 - - Add `values: [values/<name>.yaml]` to the helmfile release 78 - 4. `helmfile -l name=<name> apply` 93 + Some charts intentionally render repeated resources from values: 79 94 80 - --- 95 + - **`forward-auth`** renders one auth Deployment/Service/Ingress/Middleware set 96 + per `.Values.apps` entry. 97 + - **`external-routes`** renders off-cluster Service, EndpointSlice, and Ingress 98 + resources from `.Values.routes`. 99 + - **`pg-shared`** renders CNPG roles/databases and backup resources from chart 100 + values. 101 + - **`cluster-tls`** and **`cert-manager-issuers`** render repeated TLS or 102 + certificate resources from values. 81 103 82 - ## 🧱 Library chart 104 + Those are data charts rather than app workload charts, so a little looping is 105 + acceptable there. 83 106 84 - App templates are thin includes: 107 + ## Secret Flow 85 108 86 - ```yaml 87 - # charts/<app>/templates/deployment.yaml 88 - { { - include "common.deployment" . } } 109 + ```text 110 + secrets/foo.yaml SOPS-encrypted, multi-recipient age 111 + | 112 + v 113 + values/foo.yaml ref+sops://... refs read by vals 114 + | 115 + v 116 + helmfile values: passes plaintext values during render 117 + | 118 + v 119 + templates/secret.yaml renders a chart-local Secret 120 + | 121 + v 122 + deployment envFrom app consumes the Secret 89 123 ``` 90 124 91 - The library defines `common.deployment`, `common.service`, `common.ingress`, 92 - `common.pvc`, `common.secret`. See [`common/README.md`](common/README.md) for 93 - each partial's values reference. 94 - 95 - --- 125 + `vals` resolves `ref+sops://` URLs at render time using the local age key. 96 126 97 - ## 🚫 Charts that don't use `common/` 127 + ## Adding An App 98 128 99 - These have unique enough shape that the library wouldn't help: 129 + 1. Create `charts/<name>/Chart.yaml`. 130 + 2. Add explicit templates for only the resources the app needs, usually 131 + `deployment.yaml`, `service.yaml`, `ingress.yaml`, optional `pvc.yaml`, and 132 + optional `secret.yaml`. 133 + 3. Put app-specific behavior directly in those manifests. 134 + 4. If the app needs secrets, create `secrets/<name>.yaml` and 135 + `values/<name>.yaml`, then add that values file to the helmfile release. 136 + 5. Add the release to `../helmfile.yaml`. 137 + 6. Render before applying: 100 138 101 - - **`cluster-tls`** — renders one `kubernetes.io/tls` Secret per entry in 102 - `.Values.secret`, sourced from `secrets/cluster-tls.yaml`. 103 - - **`pg-shared`** — a CNPG `Cluster` using `local-path` volumes for Postgres 104 - data. CNPG provides HA with streaming replication across instances; B2 105 - backups and WAL archiving cover recovery. A `longhorn-pg` StorageClass is 106 - rendered for experiments or a future migration, but the live cluster does not 107 - use it. Apps that need a database get a role + database created manually in 108 - this cluster; there's no per-app provisioning yet. 109 - - **`external-routes`** — for each entry in `.Values.routes`, renders a 110 - `Service` + `EndpointSlice` + `Ingress` pointing at an external IP 111 - (typically a Tailscale IP for a service running on jubilife or eterna). 112 - Supports both traefik and tailscale ingress classes. 113 - - **`cert-manager-issuers`** — one `ClusterIssuer` (Let's Encrypt + Cloudflare 114 - DNS-01) plus one wildcard `Certificate` per entry in `.Values.wildcards`. 115 - Each cert lands as a Secret apps reference via `tlsSecret` in ingress routes. 116 - - **`longhorn-creds`** — the B2 credentials Secret that Longhorn references via 117 - `defaultBackupStore.backupTargetCredentialSecret`, plus a daily `RecurringJob` 118 - for the `default` volume group and an Ingress exposing the Longhorn UI on the 119 - tailnet. Must apply before `longhorn` (the `needs:` chain enforces this). 139 + ```bash 140 + helm template <name> charts/<name> 141 + helmfile -l name=<name> apply 142 + ```
+1 -1
k8s/charts/cert-manager-issuers/values.yaml
··· 2 2 email: aly@aly.codes 3 3 server: https://acme-v02.api.letsencrypt.org/directory # use https://acme-staging-v02.api.letsencrypt.org/directory for testing 4 4 5 - # Templated by common.secret pattern; populated from values/secrets/cert-manager.yaml. 5 + # Populated from values/secrets/cert-manager.yaml. 6 6 cloudflare: 7 7 apiToken: "" 8 8
-5
k8s/charts/common/Chart.yaml
··· 1 - apiVersion: v2 2 - name: common 3 - description: Shared template helpers for cute.haus app charts. 4 - type: library 5 - version: 0.1.0
-85
k8s/charts/common/README.md
··· 1 - # common (library chart) 2 - 3 - Shared template partials for cute.haus app charts. App charts depend on 4 - this and include the partials they need from their own `templates/*.yaml`: 5 - 6 - ```yaml 7 - { { - include "common.service" . } } 8 - ``` 9 - 10 - Deployments are hand-written per app — no shared template. All partials 11 - read from the consumer chart's `.Values` and use `.Chart.Name` for names. 12 - 13 - --- 14 - 15 - ## Partials 16 - 17 - | Define | Renders | Conditional on | 18 - | -------------------------- | --------------------------------------- | ----------------------------- | 19 - | `common.service` | `v1` Service | always | 20 - | `common.ingress` | `networking.k8s.io/v1` Ingress | `.Values.ingress.enabled` | 21 - | `common.pvc` | PVC with `helm.sh/resource-policy:keep` | `.Values.persistence.enabled` | 22 - | `common.secret` | Opaque Secret (envFrom target) | `.Values.envFromSecret` set | 23 - | `common.tailnetIngress` | Tailscale Ingress | `.Values.tailnet.enabled` | 24 - | `common.tailnetProxyClass` | Same-node Tailscale ProxyClass | `.Values.tailnet.enabled` | 25 - 26 - ### Service 27 - 28 - ```yaml 29 - service: 30 - type: ClusterIP # default ClusterIP 31 - ports: 32 - - name: http 33 - port: 80 34 - targetPort: 80 # defaults to .port; can be int or named ("http" → ports[].name) 35 - nodePort: 8282 # optional; only used when type=NodePort 36 - ``` 37 - 38 - ### Ingress 39 - 40 - ```yaml 41 - ingress: 42 - enabled: true 43 - className: traefik 44 - routes: 45 - - host: example.com 46 - aliases: [www.example.com] # optional; additional hosts under same TLS 47 - tlsSecret: example-com-tls # name of an existing kubernetes.io/tls Secret 48 - ``` 49 - 50 - The ingress backend always points at the chart's own Service on 51 - `service.ports[0].port`. 52 - 53 - ### Tailnet Ingress 54 - 55 - ```yaml 56 - tailnet: 57 - enabled: true 58 - hostname: example # defaults to .Chart.Name 59 - ingressName: example-tailnet # optional 60 - serviceName: example # defaults to .Chart.Name 61 - servicePort: 80 # defaults to service.ports[0].port if present 62 - proxyClass: 63 - name: example-same-node # optional 64 - affinity: 65 - matchLabels: # defaults to app: <chart name> 66 - app: example 67 - namespaces: [default] # defaults to release namespace 68 - ``` 69 - 70 - Renders a Tailscale `Ingress` plus a `ProxyClass` that requires the 71 - operator-managed proxy pod to schedule on the same node as pods labeled 72 - `app: <chart name>`. App charts should include both partials from their own 73 - templates. 74 - 75 - ### Secret 76 - 77 - ```yaml 78 - envFromSecret: foo-env # name of the Secret to render 79 - secret: # filled by vals from values/<chart>.yaml 80 - KEY: value 81 - ``` 82 - 83 - Renders one `Opaque` Secret with the contents of `.Values.secret` as 84 - `stringData`. The chart's `templates/secret.yaml` is just 85 - `{{- include "common.secret" . }}`.
-38
k8s/charts/common/templates/_ingress.tpl
··· 1 - {{- define "common.ingress" -}} 2 - {{- if .Values.ingress.enabled -}} 3 - apiVersion: networking.k8s.io/v1 4 - kind: Ingress 5 - metadata: 6 - name: {{ .Chart.Name }} 7 - labels: 8 - app: {{ .Chart.Name }} 9 - spec: 10 - ingressClassName: {{ .Values.ingress.className }} 11 - tls: 12 - {{- range .Values.ingress.routes }} 13 - {{- $hosts := prepend (default (list) .aliases) .host }} 14 - - hosts: 15 - {{- range $hosts }} 16 - - {{ . | quote }} 17 - {{- end }} 18 - secretName: {{ .tlsSecret }} 19 - {{- end }} 20 - rules: 21 - {{- $svcPort := .Values.ingress.servicePort | default (index $.Values.service.ports 0).port }} 22 - {{- range .Values.ingress.routes }} 23 - {{- $hosts := prepend (default (list) .aliases) .host }} 24 - {{- range $hosts }} 25 - - host: {{ . | quote }} 26 - http: 27 - paths: 28 - - path: / 29 - pathType: Prefix 30 - backend: 31 - service: 32 - name: {{ $.Chart.Name }} 33 - port: 34 - number: {{ $svcPort }} 35 - {{- end }} 36 - {{- end }} 37 - {{- end -}} 38 - {{- end -}}
-19
k8s/charts/common/templates/_pvc.tpl
··· 1 - {{- define "common.pvc" -}} 2 - {{- if and .Values.persistence .Values.persistence.enabled -}} 3 - apiVersion: v1 4 - kind: PersistentVolumeClaim 5 - metadata: 6 - name: {{ .Chart.Name }}-data 7 - labels: 8 - app: {{ .Chart.Name }} 9 - annotations: 10 - helm.sh/resource-policy: keep 11 - spec: 12 - accessModes: 13 - - ReadWriteOnce 14 - storageClassName: {{ .Values.persistence.storageClassName }} 15 - resources: 16 - requests: 17 - storage: {{ .Values.persistence.size }} 18 - {{- end -}} 19 - {{- end -}}
-21
k8s/charts/common/templates/_secret.tpl
··· 1 - {{- define "common.secret" -}} 2 - {{- $name := "" -}} 3 - {{- if .Values.managedSecret -}} 4 - {{- $name = .Values.managedSecret.name -}} 5 - {{- else if .Values.envFromSecret -}} 6 - {{- $name = index .Values.envFromSecret 0 -}} 7 - {{- end -}} 8 - {{- if $name -}} 9 - apiVersion: v1 10 - kind: Secret 11 - metadata: 12 - name: {{ $name }} 13 - labels: 14 - app: {{ .Chart.Name }} 15 - type: Opaque 16 - stringData: 17 - {{- range $k, $v := .Values.secret }} 18 - {{ $k }}: {{ $v | quote }} 19 - {{- end }} 20 - {{- end -}} 21 - {{- end -}}
-22
k8s/charts/common/templates/_service.tpl
··· 1 - {{- define "common.service" -}} 2 - apiVersion: v1 3 - kind: Service 4 - metadata: 5 - name: {{ .Chart.Name }} 6 - labels: 7 - app: {{ .Chart.Name }} 8 - spec: 9 - type: {{ .Values.service.type | default "ClusterIP" }} 10 - selector: 11 - app: {{ .Chart.Name }} 12 - ports: 13 - {{- range .Values.service.ports }} 14 - - name: {{ .name | default "http" }} 15 - port: {{ .port }} 16 - targetPort: {{ .targetPort | default .port }} 17 - protocol: {{ .protocol | default "TCP" }} 18 - {{- if .nodePort }} 19 - nodePort: {{ .nodePort }} 20 - {{- end }} 21 - {{- end }} 22 - {{- end -}}
-57
k8s/charts/common/templates/_tailnet.tpl
··· 1 - {{- define "common.tailnetProxyClass" -}} 2 - {{- if .Values.tailnet.enabled -}} 3 - {{- $proxyClass := .Values.tailnet.proxyClass | default dict -}} 4 - {{- $affinity := .Values.tailnet.affinity | default dict -}} 5 - {{- $matchLabels := $affinity.matchLabels | default (dict "app" .Chart.Name) -}} 6 - {{- $namespaces := $affinity.namespaces | default (list .Release.Namespace) -}} 7 - apiVersion: tailscale.com/v1alpha1 8 - kind: ProxyClass 9 - metadata: 10 - name: {{ $proxyClass.name | default (printf "%s-same-node" .Chart.Name) }} 11 - spec: 12 - statefulSet: 13 - pod: 14 - affinity: 15 - podAffinity: 16 - requiredDuringSchedulingIgnoredDuringExecution: 17 - - labelSelector: 18 - matchLabels: 19 - {{- range $key, $value := $matchLabels }} 20 - {{ $key }}: {{ $value }} 21 - {{- end }} 22 - namespaces: 23 - {{- range $namespaces }} 24 - - {{ . }} 25 - {{- end }} 26 - topologyKey: kubernetes.io/hostname 27 - {{- end -}} 28 - {{- end -}} 29 - 30 - {{- define "common.tailnetIngress" -}} 31 - {{- if .Values.tailnet.enabled -}} 32 - {{- $proxyClass := .Values.tailnet.proxyClass | default dict -}} 33 - {{- $ingressName := .Values.tailnet.ingressName | default (printf "%s-tailnet" .Chart.Name) -}} 34 - {{- $serviceName := .Values.tailnet.serviceName | default .Chart.Name -}} 35 - {{- $svcPort := .Values.tailnet.servicePort -}} 36 - {{- if and (not $svcPort) .Values.service -}} 37 - {{- $svcPort = (index .Values.service.ports 0).port -}} 38 - {{- end -}} 39 - apiVersion: networking.k8s.io/v1 40 - kind: Ingress 41 - metadata: 42 - name: {{ $ingressName }} 43 - labels: 44 - app: {{ .Chart.Name }} 45 - tailscale.com/proxy-class: {{ $proxyClass.name | default (printf "%s-same-node" .Chart.Name) }} 46 - spec: 47 - ingressClassName: tailscale 48 - tls: 49 - - hosts: 50 - - {{ .Values.tailnet.hostname | default .Chart.Name }} 51 - defaultBackend: 52 - service: 53 - name: {{ $serviceName }} 54 - port: 55 - number: {{ required "tailnet.servicePort is required when service.ports[0].port is not set" $svcPort }} 56 - {{- end -}} 57 - {{- end -}}
-6
k8s/charts/longhorn-creds/Chart.lock
··· 1 - dependencies: 2 - - name: common 3 - repository: file://../common 4 - version: 0.1.0 5 - digest: sha256:636a65e9846bdff17cc4e65b0849061f783759a37aa51fb85ff6fd8ba5e68467 6 - generated: "2026-06-09T21:38:50.747027918-04:00"
-4
k8s/charts/longhorn-creds/Chart.yaml
··· 3 3 description: B2 credentials Secret for Longhorn's S3-compatible backup target. 4 4 type: application 5 5 version: 0.1.0 6 - dependencies: 7 - - name: common 8 - version: 0.1.0 9 - repository: file://../common
+20 -1
k8s/charts/longhorn-creds/templates/tailscale-proxyclass.yaml
··· 1 - {{- include "common.tailnetProxyClass" . }} 1 + {{- if .Values.tailnet.enabled }} 2 + apiVersion: tailscale.com/v1alpha1 3 + kind: ProxyClass 4 + metadata: 5 + name: {{ .Values.tailnet.proxyClass.name }} 6 + spec: 7 + statefulSet: 8 + pod: 9 + affinity: 10 + podAffinity: 11 + requiredDuringSchedulingIgnoredDuringExecution: 12 + - labelSelector: 13 + matchLabels: 14 + app: {{ .Values.tailnet.affinity.matchLabels.app }} 15 + namespaces: 16 + {{- range .Values.tailnet.affinity.namespaces }} 17 + - {{ . }} 18 + {{- end }} 19 + topologyKey: kubernetes.io/hostname 20 + {{- end }}
+19 -1
k8s/charts/longhorn-creds/templates/ui-ingress.yaml
··· 1 - {{- include "common.tailnetIngress" . }} 1 + {{- if .Values.tailnet.enabled }} 2 + apiVersion: networking.k8s.io/v1 3 + kind: Ingress 4 + metadata: 5 + name: {{ .Values.tailnet.ingressName }} 6 + labels: 7 + app: longhorn-creds 8 + tailscale.com/proxy-class: {{ .Values.tailnet.proxyClass.name }} 9 + spec: 10 + ingressClassName: tailscale 11 + tls: 12 + - hosts: 13 + - {{ .Values.tailnet.hostname }} 14 + defaultBackend: 15 + service: 16 + name: {{ .Values.tailnet.serviceName }} 17 + port: 18 + number: {{ .Values.tailnet.servicePort }} 19 + {{- end }}