diff --git a/ACKNOWLEDGMENTS.md b/ACKNOWLEDGMENTS.md index 5d54426..e744308 100644 --- a/ACKNOWLEDGMENTS.md +++ b/ACKNOWLEDGMENTS.md @@ -15,3 +15,11 @@ Typesense is an open-source, typo-tolerant search engine optimized for instant s - **Repository:** [imatefx/typesense-prometheus-exporter](https://github.com/imatefx/typesense-prometheus-exporter) Provides Prometheus metrics for Typesense server instances. Used as an optional sidecar container when `metrics.enabled` is set to `true`. + +## Restic + +- **Project:** [Restic](https://restic.net) +- **Repository:** [restic/restic](https://github.com/restic/restic) +- **License:** BSD-2-Clause + +A fast, secure, and efficient backup program. Used as the backup sidecar image for scheduled Typesense data snapshots when `backup.enabled` is set to `true`. diff --git a/Changelog.md b/Changelog.md index a6fc411..20830a2 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,17 @@ # Changelog +## 1.1.0 ![AppVersion: 30.1](https://img.shields.io/static/v1?label=AppVersion&message=30.1&color=success&logo=) ![Helm: v3](https://img.shields.io/static/v1?label=Helm&message=v3&color=informational&logo=helm) + +**Release date:** 2026-03-19 + +### Added + +- Backup sidecar with restic for scheduled S3 snapshots +- Backup scripts configmap with entrypoint and backup logic +- Pod-0-only backup enforcement +- Configurable cron schedule, retention policy, and restic image +- Extra env/envFrom support for backup credentials + ## 1.0.0 ![AppVersion: 30.1](https://img.shields.io/static/v1?label=AppVersion&message=30.1&color=success&logo=) ![Helm: v3](https://img.shields.io/static/v1?label=Helm&message=v3&color=informational&logo=helm) **Release date:** 2026-03-19 diff --git a/Chart.yaml b/Chart.yaml index e553b69..2a7ab5d 100644 --- a/Chart.yaml +++ b/Chart.yaml @@ -5,7 +5,7 @@ description: >- Prometheus metrics, and Gateway API support. This chart is not officially maintained by or affiliated with the Typesense project. type: application -version: 1.0.0 +version: 1.1.0 appVersion: "30.1" icon: https://typesense.org/typesense-logo.svg home: https://github.com/hackthebox/typesense-helm @@ -37,6 +37,10 @@ annotations: image: typesense/typesense:30.1 - name: metrics-exporter image: imatefx/typesense-prometheus-exporter:v0.1.5 + - name: backup + image: ghcr.io/restic/restic:0.18.1 artifacthub.io/changes: | - kind: added - description: Initial open-source release + description: Backup sidecar with restic for scheduled S3 snapshots + - kind: added + description: Backup scripts configmap with entrypoint and backup logic diff --git a/README.md b/README.md index 5efcc50..612ae9e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # typesense -![Version: 1.0.0](https://img.shields.io/badge/Version-1.0.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 30.1](https://img.shields.io/badge/AppVersion-30.1-informational?style=flat-square) +![Version: 1.1.0](https://img.shields.io/badge/Version-1.1.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 30.1](https://img.shields.io/badge/AppVersion-30.1-informational?style=flat-square) [![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/typesense)](https://artifacthub.io/packages/search?repo=typesense) @@ -16,9 +16,11 @@ A production-ready Helm chart for deploying [Typesense](https://typesense.org), - **Security Hardened** -- Non-root container (UID 10000), read-only root filesystem, all capabilities dropped - **Gateway API & Ingress** -- Supports both Kubernetes Ingress and Gateway API (HTTPRoute) - **Prometheus Metrics** -- Optional sidecar exporter with ServiceMonitor support +- **Automated Backups** -- Optional restic-based backup sidecar with cron scheduling and retention policies - **External Secrets** -- Optional integration with the External Secrets Operator - **PodDisruptionBudget** -- Protect Raft quorum during node maintenance - **Comprehensive Tuning** -- CORS, analytics, cache, thread pools, logging, health thresholds +- **Battle-tested** -- Actively used in production on AWS EKS with S3-based backups ## TL;DR @@ -213,11 +215,55 @@ storage: > **Warning:** Reducing `storage.size` after initial deployment has no effect. PVC resize depends on your StorageClass supporting volume expansion. +### Backup + +The chart includes an optional backup sidecar that uses [restic](https://restic.net) to take scheduled snapshots of Typesense data and upload them to a remote repository (S3, GCS, Azure Blob, etc.). + +```yaml +backup: + enabled: true + intervalSeconds: 86400 # every 24 hours + retention: + keepLast: 7 + envFrom: + - secretRef: + name: restic-credentials +``` + +The `restic-credentials` Secret should contain `RESTIC_REPOSITORY`, `RESTIC_PASSWORD`, and any cloud provider credentials (e.g., `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`). + +For quick testing, you can pass credentials inline (not recommended for production): + +```yaml +backup: + enabled: true + env: + - name: RESTIC_REPOSITORY + value: s3:s3.amazonaws.com/my-bucket/typesense + - name: RESTIC_PASSWORD + value: my-restic-password +``` + +Key details: +- Backups only run on **pod-0** (other pods skip automatically) +- The restic repository is initialized automatically on the first run +- The backup sidecar runs as non-root (UID 10000) with all capabilities dropped +- See [RESTORE_RUNBOOK.md](RESTORE_RUNBOOK.md) for disaster recovery procedures + ## Values | Key | Type | Default | Description | |-----|------|---------|-------------| | affinity | object | `{}` | Affinity rules for pod scheduling | +| backup.enabled | bool | `false` | Enable the backup sidecar. Requires backup.image and restic env vars to be configured. | +| backup.env | list | `[]` | Extra env vars for the backup sidecar (e.g. RESTIC_REPOSITORY, AWS_ACCESS_KEY_ID) | +| backup.envFrom | list | `[]` | References to existing Secrets/ConfigMaps to inject into the backup sidecar | +| backup.image.pullPolicy | string | `"IfNotPresent"` | Image pull policy | +| backup.image.repository | string | `"ghcr.io/restic/restic"` | Backup sidecar image. Defaults to the official restic image which includes wget and crond via BusyBox. | +| backup.image.tag | string | `"0.18.1"` | Image tag | +| backup.intervalSeconds | int | `86400` | Interval in seconds between backup runs (default: 86400 = 24h) | +| backup.resources | object | `{"requests":{"cpu":"10m","memory":"32Mi"}}` | Resource requests and limits for the backup sidecar | +| backup.retention.keepLast | int | `7` | Number of most recent restic snapshots to keep | | extraArgs | list | `[]` | Extra command-line arguments for Typesense server (e.g., ["--filter-by-max-ops=200"]) | | extraEnv | list | `[]` | Extra environment variables for the Typesense container | | fullnameOverride | string | `""` | Override the full name of the release (optional) | diff --git a/README.md.gotmpl b/README.md.gotmpl index 51b2216..d6b92df 100644 --- a/README.md.gotmpl +++ b/README.md.gotmpl @@ -18,9 +18,11 @@ A production-ready Helm chart for deploying [Typesense](https://typesense.org), - **Security Hardened** -- Non-root container (UID 10000), read-only root filesystem, all capabilities dropped - **Gateway API & Ingress** -- Supports both Kubernetes Ingress and Gateway API (HTTPRoute) - **Prometheus Metrics** -- Optional sidecar exporter with ServiceMonitor support +- **Automated Backups** -- Optional restic-based backup sidecar with cron scheduling and retention policies - **External Secrets** -- Optional integration with the External Secrets Operator - **PodDisruptionBudget** -- Protect Raft quorum during node maintenance - **Comprehensive Tuning** -- CORS, analytics, cache, thread pools, logging, health thresholds +- **Battle-tested** -- Actively used in production on AWS EKS with S3-based backups ## TL;DR @@ -215,6 +217,41 @@ storage: > **Warning:** Reducing `storage.size` after initial deployment has no effect. PVC resize depends on your StorageClass supporting volume expansion. +### Backup + +The chart includes an optional backup sidecar that uses [restic](https://restic.net) to take scheduled snapshots of Typesense data and upload them to a remote repository (S3, GCS, Azure Blob, etc.). + +```yaml +backup: + enabled: true + intervalSeconds: 86400 # every 24 hours + retention: + keepLast: 7 + envFrom: + - secretRef: + name: restic-credentials +``` + +The `restic-credentials` Secret should contain `RESTIC_REPOSITORY`, `RESTIC_PASSWORD`, and any cloud provider credentials (e.g., `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`). + +For quick testing, you can pass credentials inline (not recommended for production): + +```yaml +backup: + enabled: true + env: + - name: RESTIC_REPOSITORY + value: s3:s3.amazonaws.com/my-bucket/typesense + - name: RESTIC_PASSWORD + value: my-restic-password +``` + +Key details: +- Backups only run on **pod-0** (other pods skip automatically) +- The restic repository is initialized automatically on the first run +- The backup sidecar runs as non-root (UID 10000) with all capabilities dropped +- See [RESTORE_RUNBOOK.md](RESTORE_RUNBOOK.md) for disaster recovery procedures + {{ template "chart.valuesSection" . }} ## Upgrading diff --git a/templates/backup-scripts-configmap.yaml b/templates/backup-scripts-configmap.yaml new file mode 100644 index 0000000..a5a6016 --- /dev/null +++ b/templates/backup-scripts-configmap.yaml @@ -0,0 +1,77 @@ +{{- if .Values.backup.enabled }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "typesense.fullname" . }}-backup-scripts + namespace: {{ .Release.Namespace }} + labels: + {{- include "typesense.labels" . | nindent 4 }} +data: + entrypoint.sh: | + #!/bin/sh + set -e + + INTERVAL="${BACKUP_INTERVAL_SECONDS}" + + echo "[backup-sidecar] Starting with interval: ${INTERVAL}s" + echo "[backup-sidecar] First backup will run in ${INTERVAL}s" + + trap 'echo "[backup-sidecar] Shutting down"; exit 0' TERM INT + + while true; do + sleep "${INTERVAL}" & + SLEEP_PID=$! + wait $SLEEP_PID || exit 0 + /scripts/backup.sh || echo "[backup-sidecar] Backup failed, will retry in ${INTERVAL}s" + done + + backup.sh: | + #!/bin/sh + set -eu + + SNAPSHOT_BASE="/usr/share/typesense/data/snapshots" + TIMESTAMP=$(date +%Y%m%dT%H%M%S) + SNAPSHOT_DIR="${SNAPSHOT_BASE}/${TIMESTAMP}" + KEEP_LAST="${BACKUP_KEEP_LAST}" + + # Only pod-0 runs the backup + ORDINAL=$(hostname | awk -F'-' '{print $NF}') + if [ "${ORDINAL}" != "0" ]; then + echo "[backup] Skipping: pod ordinal ${ORDINAL} is not 0" + exit 0 + fi + + echo "[backup] Starting backup at ${TIMESTAMP}" + + # Clean up local snapshot dir on exit (success or failure) + trap 'echo "[backup] Cleaning up ${SNAPSHOT_DIR}"; rm -rf "${SNAPSHOT_DIR}" 2>/dev/null || true' EXIT + + # Initialize restic repository if it does not exist yet + echo "[backup] Initializing restic repo if needed..." + restic snapshots --quiet > /dev/null 2>&1 || restic init + + # Remove stale restic locks from previous crashed runs + echo "[backup] Unlocking restic repo..." + restic unlock 2>/dev/null || true + + # Trigger Typesense snapshot + echo "[backup] Triggering Typesense snapshot to ${SNAPSHOT_DIR}..." + RESULT=$(wget -q -O - \ + --header="X-TYPESENSE-API-KEY: ${TYPESENSE_API_KEY}" \ + --post-data="" \ + "http://$(hostname -i):${TYPESENSE_PORT}/operations/snapshot?snapshot_path=${SNAPSHOT_DIR}") + echo "[backup] Snapshot result: ${RESULT}" + + # Validate snapshot succeeded + echo "${RESULT}" | grep -q '"success":true' || { echo "[backup] Snapshot failed: ${RESULT}"; exit 1; } + + # Upload snapshot to restic repository + echo "[backup] Running restic backup..." + restic backup "${SNAPSHOT_DIR}" --tag "typesense" --tag "${TIMESTAMP}" + + # Enforce retention policy (trap handles local cleanup) + echo "[backup] Pruning old snapshots, keeping last ${KEEP_LAST}..." + restic forget --keep-last "${KEEP_LAST}" --tag "typesense" --prune + + echo "[backup] Backup complete." +{{- end }} diff --git a/templates/statefulset.yaml b/templates/statefulset.yaml index 5b22c57..c511038 100644 --- a/templates/statefulset.yaml +++ b/templates/statefulset.yaml @@ -140,7 +140,59 @@ spec: containerPort: {{ .Values.metrics.port }} resources: {{- toYaml .Values.metrics.resources | nindent 12 }} {{- end }} + {{- if .Values.backup.enabled }} + - name: backup-sidecar + image: "{{ .Values.backup.image.repository }}:{{ .Values.backup.image.tag }}" + imagePullPolicy: {{ .Values.backup.image.pullPolicy }} + command: ["/bin/sh", "/scripts/entrypoint.sh"] + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 10000 + runAsGroup: 3000 + capabilities: + drop: + - ALL + env: + - name: BACKUP_INTERVAL_SECONDS + value: {{ .Values.backup.intervalSeconds | quote }} + - name: BACKUP_KEEP_LAST + value: {{ .Values.backup.retention.keepLast | quote }} + - name: TYPESENSE_PORT + value: {{ .Values.service.port | quote }} + - name: RESTIC_CACHE_DIR + value: /tmp/.cache/restic + {{- with .Values.backup.env }} + {{- toYaml . | nindent 12 }} + {{- end }} + envFrom: + - secretRef: + name: {{ .Values.secrets.secretName | default (printf "%s-secret" (include "typesense.fullname" .)) }} + optional: {{ .Values.secrets.optional }} + {{- with .Values.backup.envFrom }} + {{- toYaml . | nindent 12 }} + {{- end }} + volumeMounts: + - name: {{ include "typesense.fullname" . }}-data + mountPath: /usr/share/typesense/data + - name: backup-scripts + mountPath: /scripts + - name: backup-tmp + mountPath: /tmp + resources: {{- toYaml .Values.backup.resources | nindent 12 }} + {{- end }} + volumes: + {{- if .Values.backup.enabled }} + - name: backup-scripts + configMap: + name: {{ include "typesense.fullname" . }}-backup-scripts + defaultMode: 0755 + - name: backup-tmp + emptyDir: {} + {{- end }} + - name: nodeslist configMap: name: {{ include "typesense.fullname" . }}-nodeslist diff --git a/tests/backup_test.yaml b/tests/backup_test.yaml new file mode 100644 index 0000000..7390279 --- /dev/null +++ b/tests/backup_test.yaml @@ -0,0 +1,252 @@ +suite: Backup +release: + name: foo + namespace: bar +set: + replicaCount: 3 +templates: + - backup-scripts-configmap.yaml + - statefulset.yaml +tests: + - it: should not render backup scripts configmap when backup is disabled + template: backup-scripts-configmap.yaml + asserts: + - notFailedTemplate: {} + - hasDocuments: + count: 0 + + - it: should render backup scripts configmap when backup is enabled + template: backup-scripts-configmap.yaml + set: + backup: + enabled: true + image: + repository: myrepo/backup + tag: "1.0" + asserts: + - notFailedTemplate: {} + - hasDocuments: + count: 1 + - containsDocument: + apiVersion: v1 + kind: ConfigMap + name: foo-typesense-backup-scripts + namespace: bar + - isNotEmpty: + path: .data["entrypoint.sh"] + - isNotEmpty: + path: .data["backup.sh"] + + - it: should not add backup sidecar when disabled + template: statefulset.yaml + asserts: + - notFailedTemplate: {} + - lengthEqual: + path: .spec.template.spec.containers + count: 1 + + - it: should add backup sidecar when enabled + template: statefulset.yaml + set: + metrics: + enabled: false + backup: + enabled: true + image: + repository: myrepo/backup + tag: "1.0" + asserts: + - notFailedTemplate: {} + - lengthEqual: + path: .spec.template.spec.containers + count: 2 + - equal: + path: .spec.template.spec.containers[1].name + value: backup-sidecar + + - it: should mount scripts configmap in backup sidecar + template: statefulset.yaml + set: + metrics: + enabled: false + backup: + enabled: true + image: + repository: myrepo/backup + tag: "1.0" + asserts: + - notFailedTemplate: {} + - equal: + path: .spec.template.spec.containers[1].volumeMounts[1].name + value: backup-scripts + - equal: + path: .spec.template.spec.containers[1].volumeMounts[1].mountPath + value: /scripts + + - it: should mount data PVC in backup sidecar + template: statefulset.yaml + set: + metrics: + enabled: false + backup: + enabled: true + image: + repository: myrepo/backup + tag: "1.0" + asserts: + - notFailedTemplate: {} + - equal: + path: .spec.template.spec.containers[1].volumeMounts[0].mountPath + value: /usr/share/typesense/data + + - it: should pass BACKUP_INTERVAL_SECONDS and BACKUP_KEEP_LAST env vars to sidecar + template: statefulset.yaml + set: + metrics: + enabled: false + backup: + enabled: true + intervalSeconds: 3600 + retention: + keepLast: 14 + image: + repository: myrepo/backup + tag: "1.0" + asserts: + - notFailedTemplate: {} + - contains: + path: .spec.template.spec.containers[1].env + content: + name: BACKUP_INTERVAL_SECONDS + value: "3600" + - contains: + path: .spec.template.spec.containers[1].env + content: + name: BACKUP_KEEP_LAST + value: "14" + + - it: should harden backup sidecar with securityContext + template: statefulset.yaml + set: + metrics: + enabled: false + backup: + enabled: true + asserts: + - notFailedTemplate: {} + - equal: + path: .spec.template.spec.containers[1].securityContext + value: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 10000 + runAsGroup: 3000 + capabilities: + drop: + - ALL + + - it: should share typesense secret with backup sidecar + template: statefulset.yaml + set: + metrics: + enabled: false + backup: + enabled: true + image: + repository: myrepo/backup + tag: "1.0" + asserts: + - notFailedTemplate: {} + - equal: + path: .spec.template.spec.containers[1].envFrom[0].secretRef.name + value: foo-typesense-secret + + - it: should merge extra envFrom into backup sidecar + template: statefulset.yaml + set: + metrics: + enabled: false + backup: + enabled: true + image: + repository: myrepo/backup + tag: "1.0" + envFrom: + - secretRef: + name: restic-credentials + asserts: + - notFailedTemplate: {} + - equal: + path: .spec.template.spec.containers[1].envFrom[1].secretRef.name + value: restic-credentials + + - it: should render default restic image when no image is configured + template: statefulset.yaml + set: + metrics: + enabled: false + backup: + enabled: true + asserts: + - notFailedTemplate: {} + - equal: + path: .spec.template.spec.containers[1].image + value: "ghcr.io/restic/restic:0.18.1" + + - it: backup sidecar should use configured image + template: statefulset.yaml + set: + metrics: + enabled: false + backup: + enabled: true + image: + repository: myrepo/backup + tag: "1.0" + pullPolicy: Always + asserts: + - notFailedTemplate: {} + - equal: + path: .spec.template.spec.containers[1].image + value: "myrepo/backup:1.0" + - equal: + path: .spec.template.spec.containers[1].imagePullPolicy + value: Always + + - it: should place backup sidecar at containers[2] when metrics also enabled + template: statefulset.yaml + set: + metrics: + enabled: true + backup: + enabled: true + image: + repository: myrepo/backup + tag: "1.0" + asserts: + - notFailedTemplate: {} + - lengthEqual: + path: .spec.template.spec.containers + count: 3 + - equal: + path: .spec.template.spec.containers[2].name + value: backup-sidecar + + - it: should mount backup-scripts volume in pod when backup enabled + template: statefulset.yaml + set: + backup: + enabled: true + image: + repository: myrepo/backup + tag: "1.0" + asserts: + - notFailedTemplate: {} + - contains: + path: .spec.template.spec.volumes + content: + name: backup-scripts + configMap: + name: foo-typesense-backup-scripts + defaultMode: 0755 diff --git a/tests/config_test.yaml b/tests/config_test.yaml index 711a68a..1c24c4c 100644 --- a/tests/config_test.yaml +++ b/tests/config_test.yaml @@ -1,6 +1,4 @@ suite: Configuration -chart: - version: 1.0.0 release: name: foo namespace: bar diff --git a/tests/external_secret_test.yaml b/tests/external_secret_test.yaml index aff57b2..58eed3f 100644 --- a/tests/external_secret_test.yaml +++ b/tests/external_secret_test.yaml @@ -1,6 +1,4 @@ suite: Secrets -chart: - version: 1.0.0 release: name: foo namespace: bar diff --git a/tests/ingress_test.yaml b/tests/ingress_test.yaml index d16b621..2abc0a8 100644 --- a/tests/ingress_test.yaml +++ b/tests/ingress_test.yaml @@ -1,6 +1,4 @@ suite: Networking -chart: - version: 1.0.0 release: name: foo namespace: bar diff --git a/tests/pdb_test.yaml b/tests/pdb_test.yaml index e08b958..05b9598 100644 --- a/tests/pdb_test.yaml +++ b/tests/pdb_test.yaml @@ -1,6 +1,4 @@ suite: PodDisruptionBudget -chart: - version: 1.0.0 release: name: foo namespace: bar diff --git a/tests/service_test.yaml b/tests/service_test.yaml index e1c2e5a..f3e8df7 100644 --- a/tests/service_test.yaml +++ b/tests/service_test.yaml @@ -1,6 +1,4 @@ suite: Networking -chart: - version: 1.0.0 release: name: foo namespace: bar diff --git a/tests/serviceaccount_test.yaml b/tests/serviceaccount_test.yaml index 80d6a07..3b7a486 100644 --- a/tests/serviceaccount_test.yaml +++ b/tests/serviceaccount_test.yaml @@ -1,6 +1,4 @@ suite: ServiceAccount -chart: - version: 1.0.0 release: name: foo namespace: bar diff --git a/tests/servicemonitor_test.yaml b/tests/servicemonitor_test.yaml index 95f53ca..7a4cf83 100644 --- a/tests/servicemonitor_test.yaml +++ b/tests/servicemonitor_test.yaml @@ -1,6 +1,4 @@ suite: ServiceMonitor -chart: - version: 1.0.0 release: name: foo namespace: bar diff --git a/tests/statefulset_test.yaml b/tests/statefulset_test.yaml index 3e97f30..9ebe050 100644 --- a/tests/statefulset_test.yaml +++ b/tests/statefulset_test.yaml @@ -1,6 +1,4 @@ suite: App -chart: - version: 1.0.0 release: name: foo namespace: bar @@ -23,14 +21,16 @@ tests: - equal: path: .spec.replicas value: 3 - - equal: + - isSubset: path: .spec.template.metadata.labels - value: - helm.sh/chart: typesense-1.0.0 + content: app.kubernetes.io/name: typesense app.kubernetes.io/instance: foo app.kubernetes.io/version: "30.1" app.kubernetes.io/managed-by: Helm + - matchRegex: + path: .spec.template.metadata.labels["helm.sh/chart"] + pattern: ^typesense-\d+\.\d+\.\d+$ - equal: path: .spec.template.spec.securityContext diff --git a/values.md b/values.md index 5efcc50..612ae9e 100644 --- a/values.md +++ b/values.md @@ -1,6 +1,6 @@ # typesense -![Version: 1.0.0](https://img.shields.io/badge/Version-1.0.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 30.1](https://img.shields.io/badge/AppVersion-30.1-informational?style=flat-square) +![Version: 1.1.0](https://img.shields.io/badge/Version-1.1.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 30.1](https://img.shields.io/badge/AppVersion-30.1-informational?style=flat-square) [![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/typesense)](https://artifacthub.io/packages/search?repo=typesense) @@ -16,9 +16,11 @@ A production-ready Helm chart for deploying [Typesense](https://typesense.org), - **Security Hardened** -- Non-root container (UID 10000), read-only root filesystem, all capabilities dropped - **Gateway API & Ingress** -- Supports both Kubernetes Ingress and Gateway API (HTTPRoute) - **Prometheus Metrics** -- Optional sidecar exporter with ServiceMonitor support +- **Automated Backups** -- Optional restic-based backup sidecar with cron scheduling and retention policies - **External Secrets** -- Optional integration with the External Secrets Operator - **PodDisruptionBudget** -- Protect Raft quorum during node maintenance - **Comprehensive Tuning** -- CORS, analytics, cache, thread pools, logging, health thresholds +- **Battle-tested** -- Actively used in production on AWS EKS with S3-based backups ## TL;DR @@ -213,11 +215,55 @@ storage: > **Warning:** Reducing `storage.size` after initial deployment has no effect. PVC resize depends on your StorageClass supporting volume expansion. +### Backup + +The chart includes an optional backup sidecar that uses [restic](https://restic.net) to take scheduled snapshots of Typesense data and upload them to a remote repository (S3, GCS, Azure Blob, etc.). + +```yaml +backup: + enabled: true + intervalSeconds: 86400 # every 24 hours + retention: + keepLast: 7 + envFrom: + - secretRef: + name: restic-credentials +``` + +The `restic-credentials` Secret should contain `RESTIC_REPOSITORY`, `RESTIC_PASSWORD`, and any cloud provider credentials (e.g., `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`). + +For quick testing, you can pass credentials inline (not recommended for production): + +```yaml +backup: + enabled: true + env: + - name: RESTIC_REPOSITORY + value: s3:s3.amazonaws.com/my-bucket/typesense + - name: RESTIC_PASSWORD + value: my-restic-password +``` + +Key details: +- Backups only run on **pod-0** (other pods skip automatically) +- The restic repository is initialized automatically on the first run +- The backup sidecar runs as non-root (UID 10000) with all capabilities dropped +- See [RESTORE_RUNBOOK.md](RESTORE_RUNBOOK.md) for disaster recovery procedures + ## Values | Key | Type | Default | Description | |-----|------|---------|-------------| | affinity | object | `{}` | Affinity rules for pod scheduling | +| backup.enabled | bool | `false` | Enable the backup sidecar. Requires backup.image and restic env vars to be configured. | +| backup.env | list | `[]` | Extra env vars for the backup sidecar (e.g. RESTIC_REPOSITORY, AWS_ACCESS_KEY_ID) | +| backup.envFrom | list | `[]` | References to existing Secrets/ConfigMaps to inject into the backup sidecar | +| backup.image.pullPolicy | string | `"IfNotPresent"` | Image pull policy | +| backup.image.repository | string | `"ghcr.io/restic/restic"` | Backup sidecar image. Defaults to the official restic image which includes wget and crond via BusyBox. | +| backup.image.tag | string | `"0.18.1"` | Image tag | +| backup.intervalSeconds | int | `86400` | Interval in seconds between backup runs (default: 86400 = 24h) | +| backup.resources | object | `{"requests":{"cpu":"10m","memory":"32Mi"}}` | Resource requests and limits for the backup sidecar | +| backup.retention.keepLast | int | `7` | Number of most recent restic snapshots to keep | | extraArgs | list | `[]` | Extra command-line arguments for Typesense server (e.g., ["--filter-by-max-ops=200"]) | | extraEnv | list | `[]` | Extra environment variables for the Typesense container | | fullnameOverride | string | `""` | Override the full name of the release (optional) | diff --git a/values.schema.json b/values.schema.json index bfa6c4b..6b9cdd0 100644 --- a/values.schema.json +++ b/values.schema.json @@ -6,6 +6,113 @@ "required": [], "title": "affinity" }, + "backup": { + "properties": { + "enabled": { + "default": false, + "description": "Enable the backup sidecar. Requires backup.image and restic env vars to be configured.", + "title": "enabled", + "type": "boolean" + }, + "env": { + "description": "Extra env vars for the backup sidecar (e.g. RESTIC_REPOSITORY, AWS_ACCESS_KEY_ID)", + "items": { + "required": [] + }, + "required": [], + "title": "env" + }, + "envFrom": { + "description": "References to existing Secrets/ConfigMaps to inject into the backup sidecar", + "items": { + "required": [] + }, + "required": [], + "title": "envFrom" + }, + "image": { + "properties": { + "pullPolicy": { + "default": "IfNotPresent", + "description": "Image pull policy", + "required": [], + "title": "pullPolicy" + }, + "repository": { + "default": "ghcr.io/restic/restic", + "description": "Backup sidecar image. Defaults to the official restic image which includes wget and crond via BusyBox.", + "required": [], + "title": "repository" + }, + "tag": { + "default": "0.18.1", + "description": "Image tag", + "required": [], + "title": "tag" + } + }, + "required": [], + "title": "image", + "type": "object" + }, + "intervalSeconds": { + "default": 86400, + "description": "Interval in seconds between backup runs (default: 86400 = 24h)", + "minimum": 60, + "title": "intervalSeconds", + "type": "integer" + }, + "resources": { + "description": "Resource requests and limits for the backup sidecar", + "properties": { + "requests": { + "properties": { + "cpu": { + "default": "10m", + "title": "cpu", + "type": "string" + }, + "memory": { + "default": "32Mi", + "title": "memory", + "type": "string" + } + }, + "required": [ + "cpu", + "memory" + ], + "title": "requests", + "type": "object" + } + }, + "required": [ + "requests" + ], + "title": "resources" + }, + "retention": { + "properties": { + "keepLast": { + "default": 7, + "description": "Number of most recent restic snapshots to keep", + "minimum": 1, + "title": "keepLast", + "type": "integer" + } + }, + "required": [], + "title": "retention", + "type": "object" + } + }, + "required": [ + "image", + "retention" + ], + "title": "backup", + "type": "object" + }, "extraArgs": { "description": "Extra command-line arguments for Typesense server (e.g., [\"--filter-by-max-ops=200\"])", "items": { @@ -839,7 +946,8 @@ "secrets", "typesense", "serviceAccount", - "metrics" + "metrics", + "backup" ], "type": "object" } diff --git a/values.yaml b/values.yaml index 2e5e7e0..33d85f2 100644 --- a/values.yaml +++ b/values.yaml @@ -318,3 +318,39 @@ metrics: interval: 30s # -- Additional labels for ServiceMonitor labels: {} + +backup: + # @schema + # type: boolean + # @schema + # -- Enable the backup sidecar. Requires backup.image and restic env vars to be configured. + enabled: false + # @schema + # type: integer + # minimum: 60 + # @schema + # -- Interval in seconds between backup runs (default: 86400 = 24h) + intervalSeconds: 86400 + image: + # -- Backup sidecar image. Defaults to the official restic image which includes wget and crond via BusyBox. + repository: ghcr.io/restic/restic + # -- Image tag + tag: "0.18.1" + # -- Image pull policy + pullPolicy: IfNotPresent + retention: + # @schema + # type: integer + # minimum: 1 + # @schema + # -- Number of most recent restic snapshots to keep + keepLast: 7 + # -- Extra env vars for the backup sidecar (e.g. RESTIC_REPOSITORY, AWS_ACCESS_KEY_ID) + env: [] + # -- References to existing Secrets/ConfigMaps to inject into the backup sidecar + envFrom: [] + # -- Resource requests and limits for the backup sidecar + resources: + requests: + cpu: 10m + memory: 32Mi