Add a hand-written PostgreSQL chart for toolshed
Not Bitnami's: that registry has been actively unstable here (it broke Contour twice, infra issue #4) and PostgreSQL publishes no official chart. A single StatefulSet, PVC and Service is small enough that owning it costs less than depending on an unstable repackage. Credentials come from an existing Secret rather than being generated by the chart — a chart that generates its own password regenerates it on every render and silently locks you out of the existing volume. Details that matter and are easy to get wrong: - PGDATA is a subdirectory of the mount, not the mount itself. initdb refuses to run in a directory that already has contents. - Probes run through a shell. Kubernetes does not expand $(VAR) inside exec probe commands, only in command/args. - fsGroup 70 so the volume stays writable after the entrypoint drops from root to the postgres user on the Alpine variant. - shared_buffers cut to 32MB from PostgreSQL's 128MB default. The node has 8GB and was at its ceiling before this. Verified with helm template.
This commit is contained in:
@@ -0,0 +1,48 @@
|
|||||||
|
# PostgreSQL for toolshed's control plane.
|
||||||
|
#
|
||||||
|
# Deployed as shared infrastructure in its own namespace rather than inside
|
||||||
|
# the toolshed namespace, so it is addressed over cluster DNS like any other
|
||||||
|
# platform component and its lifecycle is independent of the application
|
||||||
|
# that happens to be its first consumer:
|
||||||
|
#
|
||||||
|
# postgresql.postgres.svc.cluster.local:5432
|
||||||
|
#
|
||||||
|
# Credentials come from Vault through External Secrets — see
|
||||||
|
# devops-infra-argo-config/secretstores/toolshed-postgres-credentials.yaml.
|
||||||
|
# The Secret must exist before this pod can start; a missing Secret leaves it
|
||||||
|
# in CreateContainerConfigError rather than failing in a way that explains
|
||||||
|
# itself.
|
||||||
|
|
||||||
|
fullnameOverride: postgresql
|
||||||
|
|
||||||
|
image:
|
||||||
|
repository: postgres
|
||||||
|
tag: "16-alpine"
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
|
||||||
|
existingSecret: postgresql-credentials
|
||||||
|
database: toolshed
|
||||||
|
|
||||||
|
persistence:
|
||||||
|
enabled: true
|
||||||
|
# local-path-provisioner, this cluster's default StorageClass — installed
|
||||||
|
# right after Cilium precisely because kubeadm ships no default (unlike
|
||||||
|
# k3s). 5Gi is generous for control-plane metadata; the volume is not
|
||||||
|
# resizable in place with this provisioner, so it is sized up front.
|
||||||
|
storageClass: local-path
|
||||||
|
size: 5Gi
|
||||||
|
|
||||||
|
config:
|
||||||
|
# Deliberately far below PostgreSQL's 128MB default. The node has 8GB and
|
||||||
|
# was already at its ceiling before this; the demo apps were scaled to zero
|
||||||
|
# to make room. Revisit only if query performance actually suffers, which
|
||||||
|
# for a handful of control-plane tables it will not.
|
||||||
|
sharedBuffers: 32MB
|
||||||
|
maxConnections: "50"
|
||||||
|
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 64Mi
|
||||||
|
limits:
|
||||||
|
memory: 256Mi
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
apiVersion: v2
|
||||||
|
name: postgresql
|
||||||
|
description: |
|
||||||
|
Single-instance PostgreSQL for this homelab.
|
||||||
|
|
||||||
|
Hand-written rather than vendoring Bitnami's chart: Broadcom has been
|
||||||
|
retiring and freezing images behind that repo (see claude.md infra issue
|
||||||
|
#4, where it broke Contour twice), and PostgreSQL publishes no official
|
||||||
|
Helm chart of its own. A single StatefulSet with one PVC is small enough
|
||||||
|
that owning it outright costs less than depending on an unstable
|
||||||
|
repackage.
|
||||||
|
|
||||||
|
Not highly available and not intended to be. One replica, one PVC, no
|
||||||
|
replication, no connection pooler. Adding any of those to a single-node
|
||||||
|
cluster would be theatre.
|
||||||
|
type: application
|
||||||
|
version: 0.1.0
|
||||||
|
appVersion: "16"
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{{- $name := .Values.fullnameOverride | default "postgresql" -}}
|
||||||
|
# ClusterIP only. Nothing outside the cluster should reach the database, and
|
||||||
|
# there is no Ingress here on purpose — Contour terminates HTTP, and exposing
|
||||||
|
# PostgreSQL's wire protocol through it is neither possible nor wanted.
|
||||||
|
#
|
||||||
|
# Consumers address this as:
|
||||||
|
# {{ $name }}.{{ .Release.Namespace }}.svc.cluster.local:{{ .Values.service.port }}
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: {{ $name }}
|
||||||
|
namespace: {{ .Release.Namespace }}
|
||||||
|
labels:
|
||||||
|
app: {{ $name }}
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app: {{ $name }}
|
||||||
|
ports:
|
||||||
|
- name: postgres
|
||||||
|
port: {{ .Values.service.port }}
|
||||||
|
targetPort: postgres
|
||||||
|
protocol: TCP
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
{{- $name := .Values.fullnameOverride | default "postgresql" -}}
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: StatefulSet
|
||||||
|
metadata:
|
||||||
|
name: {{ $name }}
|
||||||
|
namespace: {{ .Release.Namespace }}
|
||||||
|
labels:
|
||||||
|
app: {{ $name }}
|
||||||
|
spec:
|
||||||
|
serviceName: {{ $name }}
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: {{ $name }}
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: {{ $name }}
|
||||||
|
spec:
|
||||||
|
securityContext:
|
||||||
|
# The official image starts as root, initialises the data directory,
|
||||||
|
# then drops to the postgres user (uid 70 on the Alpine variant).
|
||||||
|
# fsGroup makes the provisioned volume group-writable so that drop
|
||||||
|
# still leaves the data directory usable.
|
||||||
|
fsGroup: 70
|
||||||
|
terminationGracePeriodSeconds: 60
|
||||||
|
containers:
|
||||||
|
- name: postgres
|
||||||
|
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||||
|
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||||
|
args:
|
||||||
|
- -c
|
||||||
|
- shared_buffers={{ .Values.config.sharedBuffers }}
|
||||||
|
- -c
|
||||||
|
- max_connections={{ .Values.config.maxConnections }}
|
||||||
|
env:
|
||||||
|
- name: POSTGRES_USER
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: {{ .Values.existingSecret }}
|
||||||
|
key: {{ .Values.secretKeys.username }}
|
||||||
|
- name: POSTGRES_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: {{ .Values.existingSecret }}
|
||||||
|
key: {{ .Values.secretKeys.password }}
|
||||||
|
- name: POSTGRES_DB
|
||||||
|
value: {{ .Values.database | quote }}
|
||||||
|
# PGDATA must be a SUBDIRECTORY of the mount, not the mount
|
||||||
|
# itself: initdb refuses to run in a directory that already has
|
||||||
|
# contents, and a freshly provisioned volume is not always empty.
|
||||||
|
- name: PGDATA
|
||||||
|
value: /var/lib/postgresql/data/pgdata
|
||||||
|
ports:
|
||||||
|
- name: postgres
|
||||||
|
containerPort: 5432
|
||||||
|
protocol: TCP
|
||||||
|
# Run through a shell so the environment expands — Kubernetes does
|
||||||
|
# not substitute $(VAR) inside exec probe commands.
|
||||||
|
readinessProbe:
|
||||||
|
exec:
|
||||||
|
command: ["sh", "-c", 'pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB" -h 127.0.0.1']
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 10
|
||||||
|
timeoutSeconds: 5
|
||||||
|
failureThreshold: 6
|
||||||
|
livenessProbe:
|
||||||
|
exec:
|
||||||
|
command: ["sh", "-c", 'pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB" -h 127.0.0.1']
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 20
|
||||||
|
timeoutSeconds: 5
|
||||||
|
failureThreshold: 6
|
||||||
|
resources:
|
||||||
|
{{- toYaml .Values.resources | nindent 12 }}
|
||||||
|
volumeMounts:
|
||||||
|
- name: data
|
||||||
|
mountPath: /var/lib/postgresql/data
|
||||||
|
{{- if .Values.persistence.enabled }}
|
||||||
|
volumeClaimTemplates:
|
||||||
|
- metadata:
|
||||||
|
name: data
|
||||||
|
spec:
|
||||||
|
accessModes: ["ReadWriteOnce"]
|
||||||
|
storageClassName: {{ .Values.persistence.storageClass | quote }}
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: {{ .Values.persistence.size | quote }}
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Chart defaults. Real configuration lives in
|
||||||
|
# helm-overrides/k8s-admin-prd-ase1/postgresql/custom-values.yaml.
|
||||||
|
|
||||||
|
fullnameOverride: postgresql
|
||||||
|
|
||||||
|
image:
|
||||||
|
# Pulled from Docker Hub, like every other infra component here (gitea,
|
||||||
|
# vault, harbor). The base-images mirror in Harbor exists to remove
|
||||||
|
# Docker Hub from the *application build* path and to minimise shipped
|
||||||
|
# app images — it is not in play for platform components.
|
||||||
|
repository: postgres
|
||||||
|
tag: "16-alpine"
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
|
||||||
|
# Name of the Secret holding username/password. Created by External
|
||||||
|
# Secrets from Vault, not by this chart — a chart that generates its own
|
||||||
|
# database password regenerates it on every render, which silently locks
|
||||||
|
# you out of an existing volume.
|
||||||
|
existingSecret: postgresql-credentials
|
||||||
|
secretKeys:
|
||||||
|
username: username
|
||||||
|
password: password
|
||||||
|
|
||||||
|
database: toolshed
|
||||||
|
|
||||||
|
service:
|
||||||
|
port: 5432
|
||||||
|
|
||||||
|
persistence:
|
||||||
|
enabled: true
|
||||||
|
storageClass: local-path
|
||||||
|
size: 5Gi
|
||||||
|
|
||||||
|
# Tuned down hard. The box has 8GB total and is at its ceiling; PostgreSQL's
|
||||||
|
# default shared_buffers of 128MB is most of this pod's budget on its own.
|
||||||
|
config:
|
||||||
|
sharedBuffers: 32MB
|
||||||
|
maxConnections: "50"
|
||||||
|
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 64Mi
|
||||||
|
limits:
|
||||||
|
memory: 256Mi
|
||||||
Reference in New Issue
Block a user