Skip to content

Commit b81581b

Browse files
authored
feat: Differential backup implementation (#243)
* feat: Differential backup implementation for postgres * feat: Added minio backup/restore in mirror mode (#244) * fix: Adjusted properties (#245) * fix: Change order to allow worker join * fix: use separate name for github token * fix: Drop section
1 parent 198a03f commit b81581b

39 files changed

+1361
-291
lines changed

.github/workflows/get-secret-from-environment.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ on:
3131
required: false
3232
BACKUP_SERVER_USER:
3333
required: false
34+
POSTGRES_USER:
35+
required: false
36+
POSTGRES_PASSWORD:
37+
required: false
3438

3539
jobs:
3640
check-environment:

.github/workflows/github-to-k8s-sync-env.yml

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,20 +32,25 @@ jobs:
3232
get-restore-env-name:
3333
name: Get restore environment name
3434
outputs:
35-
restore_env_name: ${{ steps.set-restore-env.outputs.restore-env-name }}
35+
restore_env_name: ${{ vars.RESTORE_ENVIRONMENT_NAME }}
3636
runs-on: ubuntu-24.04
3737
environment: ${{ inputs.environment }}
3838
steps:
39-
- name: Set restore environment name
40-
id: set-restore-env
39+
- name: "Env name: ${{ vars.RESTORE_ENVIRONMENT_NAME }}"
4140
run: |
42-
if [ "${{ vars.RESTORE_ENVIRONMENT_NAME }}" != "false" ]; then
43-
echo "restore-env-name=${{ vars.RESTORE_ENVIRONMENT_NAME }}"
44-
echo "restore-env-name=${{ vars.RESTORE_ENVIRONMENT_NAME }}" >> $GITHUB_OUTPUT
45-
fi
46-
- name: "Env name: ${{ steps.set-restore-env.outputs.restore-env-name }}"
41+
echo "Determined restore environment name: ${{ vars.RESTORE_ENVIRONMENT_NAME }}"
42+
get-restore-env-mode:
43+
needs: get-restore-env-name
44+
if: needs.get-restore-env-name.outputs.restore_env_name
45+
name: Get restore environment mode
46+
outputs:
47+
restore_mode: ${{ vars.BACKUP_ENVIRONMENT_MODE }}
48+
runs-on: ubuntu-24.04
49+
environment: ${{ needs.get-restore-env-name.outputs.restore_env_name }}
50+
steps:
51+
- name: "Env name: ${{ needs.get-restore-env-name.outputs.restore_env_name }} and mode ${{ vars.BACKUP_ENVIRONMENT_MODE }}"
4752
run: |
48-
echo "Determined restore environment name: ${{ steps.set-restore-env.outputs.restore-env-name }}"
53+
echo "Determined restore environment mode: ${{ vars.BACKUP_ENVIRONMENT_MODE }}"
4954
5055
get-restore-encryption-key:
5156
needs: get-restore-env-name
@@ -71,7 +76,34 @@ jobs:
7176
gh_token: ${{ secrets.GH_TOKEN }}
7277
encryption_key: ${{ secrets.GH_ENCRYPTION_PASSWORD }}
7378
BACKUP_HOST_PRIVATE_KEY: ${{ secrets.BACKUP_HOST_PRIVATE_KEY }}
74-
79+
get-restore-postgres-admin-user:
80+
needs:
81+
- get-restore-env-name
82+
- get-restore-env-mode
83+
if: needs.get-restore-env-name.outputs.restore_env_name && needs.get-restore-env-mode.outputs.restore_mode == 'differential'
84+
name: Get postgres admin user
85+
uses: ./.github/workflows/get-secret-from-environment.yml
86+
with:
87+
secret_name: 'POSTGRES_USER'
88+
env_name: ${{ needs.get-restore-env-name.outputs.restore_env_name }}
89+
secrets:
90+
gh_token: ${{ secrets.GH_TOKEN }}
91+
encryption_key: ${{ secrets.GH_ENCRYPTION_PASSWORD }}
92+
POSTGRES_USER: ${{ secrets.POSTGRES_USER }}
93+
get-restore-postgres-admin-password:
94+
needs:
95+
- get-restore-env-name
96+
- get-restore-env-mode
97+
if: needs.get-restore-env-name.outputs.restore_env_name && needs.get-restore-env-mode.outputs.restore_mode == 'differential'
98+
name: Get postgres admin password
99+
uses: ./.github/workflows/get-secret-from-environment.yml
100+
with:
101+
secret_name: 'POSTGRES_PASSWORD'
102+
env_name: ${{ needs.get-restore-env-name.outputs.restore_env_name }}
103+
secrets:
104+
gh_token: ${{ secrets.GH_TOKEN }}
105+
encryption_key: ${{ secrets.GH_ENCRYPTION_PASSWORD }}
106+
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
75107
get-restore-host:
76108
needs: get-restore-env-name
77109
if: needs.get-restore-env-name.outputs.restore_env_name
@@ -101,6 +133,8 @@ jobs:
101133
- get-restore-host
102134
- get-restore-ssh-user
103135
- get-restore-encryption-key
136+
- get-restore-postgres-admin-user
137+
- get-restore-postgres-admin-password
104138
if: always()
105139
name: Sync GitHub env to Kubernetes
106140
environment: ${{ inputs.environment }}
@@ -144,10 +178,20 @@ jobs:
144178
openssl enc -aes-256-cbc -pbkdf2 -d -salt -k "${{ secrets.GH_ENCRYPTION_PASSWORD }}" | base64)
145179
echo "::add-mask::$BACKUP_SERVER_USER"
146180
181+
POSTGRES_USER=$(echo "${{ needs.get-restore-postgres-admin-user.outputs.secret_value }}" | base64 --decode | \
182+
openssl enc -aes-256-cbc -pbkdf2 -d -salt -k "${{ secrets.GH_ENCRYPTION_PASSWORD }}" | base64)
183+
echo "::add-mask::$POSTGRES_USER"
184+
185+
POSTGRES_PASSWORD=$(echo "${{ needs.get-restore-postgres-admin-password.outputs.secret_value }}" | base64 --decode | \
186+
openssl enc -aes-256-cbc -pbkdf2 -d -salt -k "${{ secrets.GH_ENCRYPTION_PASSWORD }}" | base64)
187+
echo "::add-mask::$POSTGRES_PASSWORD"
188+
147189
echo RESTORE_ENCRYPTION_PASSPHRASE=$RESTORE_ENCRYPTION_PASSPHRASE >> $env_file
148190
echo BACKUP_HOST=$BACKUP_HOST >> $env_file
149191
echo BACKUP_HOST_PRIVATE_KEY=$BACKUP_HOST_PRIVATE_KEY >> $env_file
150192
echo BACKUP_SERVER_USER=$BACKUP_SERVER_USER >> $env_file
193+
echo POSTGRES_USER=$POSTGRES_USER >> $env_file
194+
echo POSTGRES_PASSWORD=$POSTGRES_PASSWORD >> $env_file
151195
grep BACKUP_HOST_PRIVATE_KEY $env_file || echo "No restore encryption passphrase to add"
152196
153197
- name: Preprocess mapping into Secret YAMLs
@@ -185,7 +229,7 @@ jobs:
185229
k8s_var="$key"
186230
fi
187231
188-
value=$(grep "^$github_var=" "$env_file" | cut -d= -f2- || true)
232+
value=$(grep "^$github_var=" "$env_file" | cut -d= -f2- | tail -1 || true)
189233
190234
if [ -n "$value" ]; then
191235
echo " $k8s_var: $value"

charts/dependencies/Chart.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@ apiVersion: v2
22
name: opencrvs-dependencies-chart
33
description: Dependencies required by OpenCRVS
44
type: application
5-
version: 0.2.7
5+
version: 0.2.9
66
appVersion: v1.9.10

charts/dependencies/README.md

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,22 @@ This section allows you to configure the postgres deployment within your infrast
7373
| storage_type | string | `pvc` | Kubernetes storage type, available options are `pvc` or `host_path`. More information are at [Storage Configuration](#storage-configuration) |
7474
| host_data_path | string | `/data/postgres` | Path to persistent data on VM (host) |
7575
| node_selector | dict | `{}` | Label selector for datastore nodes, usually used to keep data persistent |
76-
| backup_schedule | string | `n/a` | Backup cronjob schedule, if not defined then values from `backup.schedule` is used |
77-
| backup_server_dir | string | `n/a` | Directory to store encrypted backup on backup server, if not defined `backup.backup_server_dir` is used |
76+
| backup.{} | dict | `{}` | Backup configuration section, for more information please check `values.yaml` and **Backup section** in this README |
77+
| backup.enabled | string | `false` | Backup enabled or disabled, section has higher priority over global `backup` section |
78+
| backup.type | string | `dump` | `dump` is a full logical database dump, `differential` is a physical backup using pgBackRest |
79+
| backup.server_secret | string | `backup-server-ssh-credentials` | Name of the Kubernetes secret with backup server credentials |
80+
| backup.encryption_secret | string | `backup-encryption-secret` | Name of the Kubernetes secret containing the backup encryption key |
81+
| backup.schedule | dict | `{}` | Backup cronjob schedule |
82+
| backup.schedule.dump | string | `0 1 * * *` | Used only when type=dump, if not defined then value from `backup.schedule` is used |
83+
| backup.schedule.full | string | `0 1 * * 0` | Full backup schedule. Used when type=differential, note that value from `backup.schedule` is ignored |
84+
| `backup.schedule.differential` | string | `0 1 * * 1-6` | Differential backup schedule. Used when type=differential, note that value from `backup.schedule` is ignored |
85+
| backup.server_dir | string | `n/a` | Directory to store encrypted backup on backup server, if not defined `backup.backup_server_dir` is used |
86+
| restore.{} | dict | `{}` | Restore configuration section, for more information please check `values.yaml` and **Restore section** in this README |
87+
| restore.enabled | string | `false` | Restore enabled or disabled, section has higher priority over global `restore` section |
88+
| restore.server_secret | string | `backup-server-ssh-credentials` | Name of the Kubernetes secret with backup server credentials, usually backup server is used for restore, thats why credentials are shared |
89+
| restore.encryption_secret | string | `restore-encryption-secret` | Name of the Kubernetes secret containing the backup encryption key |
90+
| restore.schedule | string | `0 3 * * *` | Restore cronjob schedule, if not defined then value from `restore.schedule` is used |
91+
7892

7993
## Elasticsearch
8094

@@ -89,11 +103,11 @@ This section allows you to configure the deployment and authentication settings
89103
| storage_type | string | `pvc` | Kubernetes storage type, available options are `pvc` or `host_path`. More information are at [Storage Configuration](#storage-configuration) |
90104
| host_data_path | string | `/data/elasticsearch` | Path to persistent data on VM (host) |
91105
| node_selector | dict | `{}` | Label selector for datastore nodes, usually used to keep data persistent |
92-
| backup_schedule | string | `n/a` | Backup cronjob schedule, if not defined then values from `backup.schedule` is used |
93-
| backup_server_dir | string | `n/a` | Directory to store encrypted backup on backup server, if not defined `backup.backup_server_dir` is used |
106+
94107

95108
## MinIO
96109

110+
### Configuration options
97111
| Key | Default value | Description |
98112
|-|-|-|
99113
| enabled | true | Enable or disable minio service |
@@ -102,9 +116,19 @@ This section allows you to configure the deployment and authentication settings
102116
| storage_type | string | `pvc` | Kubernetes storage type, available options are `pvc` or `host_path`. More information are at [Storage Configuration](#storage-configuration) |
103117
| host_data_path | string | `/data/minio` | Path to persistent data on VM (host) |
104118
| node_selector | dict | `{}` | Label selector for datastore nodes, usually used to keep data persistent |
105-
| backup_schedule | string | `n/a` | Backup cronjob schedule, if not defined then values from `backup.schedule` is used |
106-
| backup_server_dir | string | `n/a` | Directory to store encrypted backup on backup server, if not defined `backup.backup_server_dir` is used |
107-
119+
| backup.{} | dict | `{}` | Backup configuration section, for more information please check `values.yaml` and **Backup section** in this README |
120+
| backup.enabled | string | `false` | Backup enabled or disabled, section has higher priority over global `backup` section |
121+
| backup.type | string | `dump` | `dump` is a full filesystem dump, `differential` is rsync from MinIO filesystem on remote backup server |
122+
| backup.server_secret | string | `backup-server-ssh-credentials` | Name of the Kubernetes secret with backup server credentials |
123+
| backup.schedule | string | `0 1 * * *` | Time to run backup job, if not defined then value from `backup.schedule` is used |
124+
| backup.server_dir | string | `n/a` | Directory on backup server for encrypted archive backups or filesystem rsync. Uses global value if not set |
125+
| restore.{} | dict | `{}` | Restore configuration section, for more information please check `values.yaml` and **Restore section** in this README |
126+
| restore.enabled | string | `false` | Enables restore functionality; section overrides global `restore` settings. |
127+
| restore.type | string | `dump` | Restore method: `dump` (from encrypted archive) or `differential` (same as for backup) |
128+
| restore.server_secret | string | `backup-server-ssh-credentials` | Name of the Kubernetes secret with backup server credentials, usually backup server is used for restore, thats why credentials are shared |
129+
| restore.schedule | string | `0 3 * * *` | Restore cronjob schedule, if not defined then value from `restore.schedule` is used |
130+
131+
### MinIO Credentials
108132
Setting `use_default_credentials` to `false` will generate strong password for MinIO.
109133

110134
MinIO defaults to minioadmin and minioadmin as the access key and secret key respectively.
@@ -141,6 +165,10 @@ documents:
141165
- MINIO_SECRET_KEY
142166
```
143167
168+
### Backup and Restore Section Reference
169+
170+
For detailed configuration, review the values.yaml file and refer to the Backup and Restore sections of this README.
171+
Adjust schedules, server credentials, and directories as needed for your deployment.
144172
145173
## Redis
146174

charts/dependencies/files/minio/backup.sh

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ REMOTE_DIR="${BACKUP_REMOTE_DIR:-"/home/$BACKUP_USER"}/$BACKUP_DATE"
2424
# Number of retries for backup creation
2525
MAX_RETRIES=10
2626

27-
BACKUP_MODE=${BACKUP_MODE:-"fs"}
2827
# Install required tools
2928
apk add --no-cache bash curl openssl openssh jq rsync minio-client
3029

@@ -33,9 +32,9 @@ if [ -z "$ENCRYPT_PASS" ]; then
3332
echo "[$(date +%F\ %H:%M:%S)] [ERROR] Must provide ENCRYPT_PASS environment variable"
3433
exit 1
3534
fi
35+
3636
# Mirror data before backup
37-
# Works well on any minio installation but is slower and requires additional disk space
38-
backup_mirror(){
37+
backup_and_archive(){
3938
MINIO_ALIAS=local
4039
mcli alias set $MINIO_ALIAS http://minio:3535 $MINIO_ROOT_USER $MINIO_ROOT_PASSWORD
4140

@@ -55,13 +54,6 @@ backup_mirror(){
5554
cd $BACKUP_PATH && tar -zcvf $ARCHIVE_PATH .
5655
}
5756

58-
# Backup entire filesystem
59-
# Works well on single node installation and is fastest way to create backup
60-
backup_fs(){
61-
echo "[$(date +%F\ %H:%M:%S)] Archive backup at $ARCHIVE_PATH"
62-
cd /data && tar -zcvf $ARCHIVE_PATH .
63-
}
64-
6557
create_encrypted_backup(){
6658
echo "[$(date +%F\ %H:%M:%S)] Encrypt backup at $ARCHIVE_PATH"
6759
openssl enc -aes-256-cbc -pbkdf2 -salt -in "$ARCHIVE_PATH" -out "${ARCHIVE_PATH}.enc" -pass env:ENCRYPT_PASS
@@ -88,11 +80,8 @@ echo "[$(date +%F\ %H:%M:%S)] Running backup container"
8880
echo "[$(date +%F\ %H:%M:%S)] Setup connection to container http://minio:3535"
8981

9082

91-
if [ $BACKUP_MODE == "fs" ]; then
92-
backup_fs
93-
elif [ $BACKUP_MODE == "mirror" ]; then
94-
backup_mirror
95-
fi
83+
backup_and_archive
84+
9685
create_encrypted_backup
9786

9887
transfer_to_backup_host

charts/dependencies/files/minio/restore.sh

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@ ARCHIVE_PATH="/tmp/$ARCHIVE_NAME"
2727

2828
# Remote directory on backup server
2929
REMOTE_DIR="${BACKUP_REMOTE_DIR:-"/home/$BACKUP_USER"}/$RESTORE_DATE"
30-
# Default is filesystem restore, can be "mirror"
31-
RESTORE_MODE=${RESTORE_MODE:-"fs"}
3230

3331
if [ -z "$ENCRYPT_PASS" ]; then
3432
echo "[$(date +%F\ %H:%M:%S)] [ERROR] Must provide ENCRYPT_PASS environment variable"
@@ -44,15 +42,7 @@ decrypt_backup() {
4442
echo "[$(date +%F\ %H:%M:%S)] Decrypted archive at $ARCHIVE_PATH"
4543
}
4644

47-
# Restore files from filesystem backup
48-
restore_fs() {
49-
echo "[$(date +%F\ %H:%M:%S)] Restoring MinIO data using filesystem method"
50-
rm -rf "${RESTORE_DIR:?}"/* # Clean current contents!
51-
tar -zxvf "$ARCHIVE_PATH" -C "$RESTORE_DIR"
52-
echo "[$(date +%F\ %H:%M:%S)] Restore of $RESTORE_DIR complete"
53-
}
54-
55-
# Step 2 (alternative). Restore using MinIO mirror (bucket by bucket)
45+
# Restore using MinIO mirror (bucket by bucket)
5646
restore_mirror() {
5747
MINIO_ALIAS=local-restore
5848
mcli alias set $MINIO_ALIAS http://minio:3535 $MINIO_ROOT_USER $MINIO_ROOT_PASSWORD
@@ -95,13 +85,6 @@ transfer_from_backup_host
9585

9686
decrypt_backup
9787

98-
if [ "$RESTORE_MODE" == "fs" ]; then
99-
restore_fs
100-
elif [ "$RESTORE_MODE" == "mirror" ]; then
101-
restore_mirror
102-
else
103-
echo "[$(date +%F\ %H:%M:%S)] [ERROR] Unknown RESTORE_MODE: $RESTORE_MODE" >&2
104-
exit 1
105-
fi
88+
restore_mirror
10689

10790
echo "[$(date +%F\ %H:%M:%S)] MinIO restore process completed successfully"
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
#!/bin/bash
2+
3+
# This Source Code Form is subject to the terms of the Mozilla Public
4+
# License, v. 2.0. If a copy of the MPL was not distributed with this
5+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
6+
#
7+
# OpenCRVS is also distributed under the terms of the Civil Registration
8+
# & Healthcare Disclaimer located at http://opencrvs.org/license.
9+
#
10+
# Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS.
11+
12+
set -e
13+
14+
15+
common_config(){
16+
apt update -q
17+
apt upgrade -y -q
18+
apt install -y -q pgbackrest openssh-client
19+
# Common configuration for backup and restore
20+
# Temporal directory required for pushing WAL files
21+
echo "Create pgbackrest temp directory with correct permissions"
22+
mkdir -p /tmp/pgbackrest
23+
chown postgres:postgres /tmp/pgbackrest
24+
chmod 770 /tmp/pgbackrest
25+
26+
# Skip ssh keys validation for external connections
27+
echo "Configure key-based access to backup server"
28+
{
29+
echo "StrictHostKeyChecking no"
30+
echo "UserKnownHostsFile /dev/null"
31+
} > /etc/ssh/ssh_config.d/backup.conf
32+
33+
34+
# Create home directory for postgres user and push ssh keys inside
35+
mkdir -p /var/lib/postgresql/.ssh
36+
cp /root/.ssh/id_rsa /var/lib/postgresql/.ssh/id_rsa
37+
chown -R postgres:postgres /var/lib/postgresql/.ssh/id_rsa
38+
chmod 400 /var/lib/postgresql/.ssh/id_rsa
39+
40+
}
41+
42+
backup_mode(){
43+
44+
if [ -f "${PGDATA}/postgresql.conf" ]
45+
then
46+
echo "Database type: $DB_TYPE. Modify ${PGDATA}/postgresql.conf"
47+
grep -q "pgbackrest --stanza=$PGBACKREST_STANZA" ${PGDATA}/postgresql.conf || \
48+
cat >> ${PGDATA}/postgresql.conf <<EOF
49+
50+
# WAL Archive Configuration
51+
archive_mode = on
52+
archive_command = 'pgbackrest --stanza=$PGBACKREST_STANZA archive-push %p'
53+
archive_timeout = 60
54+
max_wal_senders = 3
55+
wal_keep_size = 1GB
56+
max_wal_size = 2GB
57+
min_wal_size = 1GB
58+
EOF
59+
else
60+
echo "Warning: '${PGDATA}/postgresql.conf' not found, cannot configure backup."
61+
fi
62+
63+
}
64+
65+
restore_mode(){
66+
67+
# pgBackRest does restore filesystem from production including configuration files
68+
# Drop backup section from postgresql.conf after database restore
69+
if [ -f ${PGDATA}/postgresql.conf ] && [ "$DB_TYPE" == "restore" ]
70+
then
71+
echo "Database type: $DB_TYPE. Modify ${PGDATA}/postgresql.conf: Drop archive settings."
72+
grep -v 'archive_' ${PGDATA}/postgresql.conf > ${PGDATA}/postgresql.conf.tmp
73+
mv ${PGDATA}/postgresql.conf.tmp ${PGDATA}/postgresql.conf
74+
else
75+
echo "Warning: '${PGDATA}/postgresql.conf' not found, cannot clean archive settings."
76+
fi
77+
}
78+
79+
###########################################################
80+
# How it works?
81+
# 1. standalone: database without backup or restore configured and
82+
# backup/restore.type=dump.
83+
# 2. backup: backup.type=differential.
84+
# Production database is accessible for write while backup
85+
# WAL is used to play transactions made while full/diff backup
86+
# 3. restore: restore.type=differential.
87+
# After restore drop WAL section to avoid concurrent write
88+
# operations to production repository
89+
###########################################################
90+
case "${DB_TYPE:-standalone}" in
91+
standalone)
92+
echo "Database type: $DB_TYPE. Additional configuration is not required" && \
93+
exit 0
94+
;;
95+
backup)
96+
common_config
97+
backup_mode
98+
;;
99+
restore)
100+
common_config
101+
restore_mode
102+
;;
103+
*)
104+
log "Unknown or missing DB_TYPE: '${DB_TYPE}'. Must be standalone, backup, or restore."
105+
exit 1
106+
;;
107+
esac
108+

0 commit comments

Comments
 (0)