feat: Implement S3 to Storage Box backup

Introduces a daily Kubernetes CronJob that utilizes rclone to perform compressed backups of oCIS S3 data to a Hetzner Storage Box via SFTP.

This new backup mechanism requires the manual creation of an 'ocis-storagebox-credentials' secret, which holds the Storage Box host, user, and SSH private key. A check is added to the secret initialization job to ensure this essential external secret exists.
This commit is contained in:
Felix Wolf 2026-04-06 15:24:14 +02:00
parent a3143ac33c
commit 1122c3f0e2
9 changed files with 229 additions and 3 deletions

View file

@ -81,4 +81,5 @@ kubectl apply -f rendered/envs/production/<app>/ --server-side # Deploy
- When adding a new application that uses a Helm chart generating secrets, configure all `secretRefs` to point to pre-created secret names and use an init Job to generate them.
- Known external secrets (not in git, created manually):
- `ocis/ocis-s3-credentials` — Hetzner S3 access key and secret key
- `ocis/ocis-storagebox-credentials` — Hetzner Storage Box host, user, and SSH private key (for S3 backup to Helsinki)
- `cert-manager/letsencrypt-account-key` — ACME account key (auto-generated by cert-manager)

View file

@ -0,0 +1,109 @@
#@ load("@ytt:data", "data")
#@ ns = data.values.application.namespace
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: ocis-s3-backup
namespace: #@ ns
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: ocis-s3-backup
namespace: #@ ns
spec:
schedule: "0 2 * * *"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
ttlSecondsAfterFinished: 86400
template:
spec:
restartPolicy: OnFailure
serviceAccountName: ocis-s3-backup
containers:
- name: backup
image: alpine:3.20
resources:
requests:
memory: 128Mi
cpu: 50m
command:
- sh
- -c
- |
set -e
apk add --no-cache rclone >/dev/null 2>&1
mkdir -p /tmp/rclone
cat > /tmp/rclone/rclone.conf <<CONF
[s3]
type = s3
provider = Other
access_key_id = ${S3_ACCESS_KEY}
secret_access_key = ${S3_SECRET_KEY}
endpoint = https://nbg1.your-objectstorage.com
acl = private
[storagebox]
type = sftp
host = ${STORAGEBOX_HOST}
port = 23
user = ${STORAGEBOX_USER}
key_file = /etc/storagebox/ssh-key
shell_type = none
md5sum_command = none
sha1sum_command = none
[backup]
type = compress
remote = storagebox:ocis-backup
CONF
echo "Syncing S3 bucket to Storage Box (compressed)..."
rclone sync s3:ocis-tr1ceracop backup: \
--config /tmp/rclone/rclone.conf \
--transfers 4 \
-v
rm -rf /tmp/rclone
echo "Backup complete."
env:
- name: S3_ACCESS_KEY
valueFrom:
secretKeyRef:
name: ocis-s3-credentials
key: accessKey
- name: S3_SECRET_KEY
valueFrom:
secretKeyRef:
name: ocis-s3-credentials
key: secretKey
- name: STORAGEBOX_HOST
valueFrom:
secretKeyRef:
name: ocis-storagebox-credentials
key: host
- name: STORAGEBOX_USER
valueFrom:
secretKeyRef:
name: ocis-storagebox-credentials
key: user
volumeMounts:
- name: storagebox-ssh
mountPath: /etc/storagebox
readOnly: true
volumes:
- name: storagebox-ssh
secret:
secretName: ocis-storagebox-credentials
items:
- key: ssh-key
path: ssh-key
defaultMode: 0400

View file

@ -96,6 +96,11 @@ spec:
exit 1
fi
if ! kubectl get secret ocis-storagebox-credentials -n "${NAMESPACE}" >/dev/null 2>&1; then
echo "ERROR: External secret ocis-storagebox-credentials must be created manually"
exit 1
fi
# Admin user
create_secret_if_missing ocis-admin-user \
--from-literal=password="$(gen_random 32)" \

View file

@ -1,6 +1,6 @@
apiVersion: v1
data:
service-account-id: 1a1c862d-11c0-4c04-b078-c20277e455f5
service-account-id: 2387fad3-be34-4b10-948b-421873985560
kind: ConfigMap
metadata:
annotations:

View file

@ -1,6 +1,6 @@
apiVersion: v1
data:
application-id: 802e4fae-4e57-40ff-8c3e-fd92717b2ac3
application-id: d019e54c-51c8-46ab-aded-87182aafcee4
kind: ConfigMap
metadata:
annotations:

View file

@ -1,6 +1,6 @@
apiVersion: v1
data:
storage-uuid: a8925d16-9aea-4c77-923a-49bb7d8eeb77
storage-uuid: 30a27136-b87a-431f-9d0d-0cfec28061e4
kind: ConfigMap
metadata:
annotations:

View file

@ -0,0 +1,99 @@
apiVersion: batch/v1
kind: CronJob
metadata:
annotations:
a8r.io/repository: ssh://git@git.tr1ceracop.de:222/gitea_admin/k8s-and-chill.git
name: ocis-s3-backup
namespace: ocis
spec:
concurrencyPolicy: Forbid
failedJobsHistoryLimit: 3
jobTemplate:
spec:
template:
spec:
containers:
- command:
- sh
- -c
- |
set -e
apk add --no-cache rclone >/dev/null 2>&1
mkdir -p /tmp/rclone
cat > /tmp/rclone/rclone.conf <<CONF
[s3]
type = s3
provider = Other
access_key_id = ${S3_ACCESS_KEY}
secret_access_key = ${S3_SECRET_KEY}
endpoint = https://nbg1.your-objectstorage.com
acl = private
[storagebox]
type = sftp
host = ${STORAGEBOX_HOST}
port = 23
user = ${STORAGEBOX_USER}
key_file = /etc/storagebox/ssh-key
shell_type = none
md5sum_command = none
sha1sum_command = none
[backup]
type = compress
remote = storagebox:ocis-backup
CONF
echo "Syncing S3 bucket to Storage Box (compressed)..."
rclone sync s3:ocis-tr1ceracop backup: \
--config /tmp/rclone/rclone.conf \
--transfers 4 \
-v
rm -rf /tmp/rclone
echo "Backup complete."
env:
- name: S3_ACCESS_KEY
valueFrom:
secretKeyRef:
key: accessKey
name: ocis-s3-credentials
- name: S3_SECRET_KEY
valueFrom:
secretKeyRef:
key: secretKey
name: ocis-s3-credentials
- name: STORAGEBOX_HOST
valueFrom:
secretKeyRef:
key: host
name: ocis-storagebox-credentials
- name: STORAGEBOX_USER
valueFrom:
secretKeyRef:
key: user
name: ocis-storagebox-credentials
image: alpine:3.20
name: backup
resources:
requests:
cpu: 50m
memory: 128Mi
volumeMounts:
- mountPath: /etc/storagebox
name: storagebox-ssh
readOnly: true
restartPolicy: OnFailure
serviceAccountName: ocis-s3-backup
volumes:
- name: storagebox-ssh
secret:
defaultMode: 256
items:
- key: ssh-key
path: ssh-key
secretName: ocis-storagebox-credentials
ttlSecondsAfterFinished: 86400
schedule: 0 2 * * *
successfulJobsHistoryLimit: 3

View file

@ -45,6 +45,11 @@ spec:
exit 1
fi
if ! kubectl get secret ocis-storagebox-credentials -n "${NAMESPACE}" >/dev/null 2>&1; then
echo "ERROR: External secret ocis-storagebox-credentials must be created manually"
exit 1
fi
# Admin user
create_secret_if_missing ocis-admin-user \
--from-literal=password="$(gen_random 32)" \

View file

@ -0,0 +1,7 @@
apiVersion: v1
kind: ServiceAccount
metadata:
annotations:
a8r.io/repository: ssh://git@git.tr1ceracop.de:222/gitea_admin/k8s-and-chill.git
name: ocis-s3-backup
namespace: ocis