This commit is contained in:
2025-12-23 15:41:08 +00:00
parent 203a3f9b71
commit 959305c93c
5 changed files with 257 additions and 227 deletions

View File

@@ -10,231 +10,254 @@ in {
options.my.pgbackrest = {
enable = mkEnableOption "pgBackRest PostgreSQL backup";
stanza = mkOption {
type = types.str;
default = "main";
description = "Name of the pgBackRest stanza";
};
secretFile = mkOption {
type = types.path;
description = "Path to the environment file containing S3 credentials and cipher passphrase";
};
s3 = mkOption {
type = types.submodule {
options = {
endpoint = mkOption {
type = types.str;
default = "s3.eu-central-003.backblazeb2.com";
description = "S3 endpoint URL";
};
bucket = mkOption {
type = types.str;
description = "S3 bucket name";
};
region = mkOption {
type = types.str;
default = "eu-central-003";
description = "S3 region";
};
path = mkOption {
type = types.str;
default = "/backups";
description = "Path within the S3 bucket";
};
};
stanza =
mkOption {
type = types.str;
default = "main";
description = "Name of the pgBackRest stanza";
};
default = {};
description = "S3 storage configuration";
};
retention = mkOption {
type = types.submodule {
options = {
full = mkOption {
type = types.int;
default = 7;
description = "Number of full backups to retain";
};
diff = mkOption {
type = types.int;
default = 7;
description = "Number of differential backups to retain";
};
};
secretFile =
mkOption {
type = types.path;
description = "Path to the environment file containing S3 credentials and cipher passphrase";
};
default = {};
description = "Backup retention configuration";
};
compression = mkOption {
type = types.submodule {
options = {
type = mkOption {
type = types.str;
default = "zst";
description = "Compression algorithm (none, gz, lz4, zst)";
};
s3 =
mkOption {
type =
types.submodule {
options = {
endpoint =
mkOption {
type = types.str;
default = "s3.eu-central-003.backblazeb2.com";
description = "S3 endpoint URL";
};
level = mkOption {
type = types.int;
default = 3;
description = "Compression level";
bucket =
mkOption {
type = types.str;
description = "S3 bucket name";
};
region =
mkOption {
type = types.str;
default = "eu-central-003";
description = "S3 region";
};
path =
mkOption {
type = types.str;
default = "/backups";
description = "Path within the S3 bucket";
};
};
};
};
default = {};
description = "S3 storage configuration";
};
default = {};
description = "Compression configuration";
};
processMax = mkOption {
type = types.int;
default = 2;
description = "Maximum number of processes for parallel operations";
};
retention =
mkOption {
type =
types.submodule {
options = {
full =
mkOption {
type = types.int;
default = 7;
description = "Number of full backups to retain";
};
schedule = mkOption {
type = types.submodule {
options = {
full = mkOption {
type = types.str;
default = "daily";
description = "OnCalendar expression for full backups";
diff =
mkOption {
type = types.int;
default = 7;
description = "Number of differential backups to retain";
};
};
};
diff = mkOption {
type = types.str;
default = "hourly";
description = "OnCalendar expression for differential backups";
};
};
default = {};
description = "Backup retention configuration";
};
compression =
mkOption {
type =
types.submodule {
options = {
type =
mkOption {
type = types.str;
default = "zst";
description = "Compression algorithm (none, gz, lz4, zst)";
};
level =
mkOption {
type = types.int;
default = 3;
description = "Compression level";
};
};
};
default = {};
description = "Compression configuration";
};
processMax =
mkOption {
type = types.int;
default = 2;
description = "Maximum number of processes for parallel operations";
};
schedule =
mkOption {
type =
types.submodule {
options = {
full =
mkOption {
type = types.str;
default = "daily";
description = "OnCalendar expression for full backups";
};
diff =
mkOption {
type = types.str;
default = "hourly";
description = "OnCalendar expression for differential backups";
};
};
};
default = {};
description = "Backup schedule configuration";
};
default = {};
description = "Backup schedule configuration";
};
};
config = mkIf cfg.enable (let
archivePushScript = pkgs.writeShellScript "pgbackrest-archive-push" ''
set -a
source ${cfg.secretFile}
set +a
exec ${pkgs.pgbackrest}/bin/pgbackrest --stanza=${cfg.stanza} archive-push "$1"
'';
in {
environment.systemPackages = [
pkgs.pgbackrest
(pkgs.writeShellScriptBin "pgbackrest-wrapper" ''
set -a
source ${cfg.secretFile}
set +a
exec ${pkgs.pgbackrest}/bin/pgbackrest "$@"
'')
];
config =
mkIf cfg.enable (let
archivePushScript =
pkgs.writeShellScript "pgbackrest-archive-push" ''
set -a
source ${cfg.secretFile}
set +a
exec ${pkgs.pgbackrest}/bin/pgbackrest --stanza=${cfg.stanza} archive-push "$1"
'';
in {
environment.systemPackages = [
pkgs.pgbackrest
(pkgs.writeShellScriptBin "pgbackrest-wrapper" ''
set -a
source ${cfg.secretFile}
set +a
exec ${pkgs.pgbackrest}/bin/pgbackrest "$@"
'')
];
services.postgresql.settings = {
archive_mode = "on";
archive_command = "${archivePushScript} %p";
};
environment.etc."pgbackrest/pgbackrest.conf".text = ''
[global]
repo1-type=s3
repo1-s3-endpoint=${cfg.s3.endpoint}
repo1-s3-bucket=${cfg.s3.bucket}
repo1-s3-region=${cfg.s3.region}
repo1-path=${cfg.s3.path}
repo1-retention-full=${toString cfg.retention.full}
repo1-retention-diff=${toString cfg.retention.diff}
repo1-cipher-type=aes-256-cbc
compress-type=${cfg.compression.type}
compress-level=${toString cfg.compression.level}
process-max=${toString cfg.processMax}
log-level-console=info
log-level-file=detail
log-path=/var/log/pgbackrest
spool-path=/var/spool/pgbackrest
[${cfg.stanza}]
pg1-path=/var/lib/postgresql/${config.services.postgresql.package.psqlSchema}
pg1-user=postgres
'';
systemd.services.pgbackrest-stanza-create = {
description = "pgBackRest Stanza Create";
after = ["postgresql.service"];
requires = ["postgresql.service"];
path = [pkgs.pgbackrest];
serviceConfig = {
Type = "oneshot";
User = "postgres";
EnvironmentFile = cfg.secretFile;
RemainAfterExit = true;
services.postgresql.settings = {
archive_mode = "on";
archive_command = "${archivePushScript} %p";
};
script = ''
pgbackrest --stanza=${cfg.stanza} stanza-create || true
environment.etc."pgbackrest/pgbackrest.conf".text = ''
[global]
repo1-type=s3
repo1-s3-endpoint=${cfg.s3.endpoint}
repo1-s3-bucket=${cfg.s3.bucket}
repo1-s3-region=${cfg.s3.region}
repo1-path=${cfg.s3.path}
repo1-retention-full=${toString cfg.retention.full}
repo1-retention-diff=${toString cfg.retention.diff}
repo1-cipher-type=aes-256-cbc
compress-type=${cfg.compression.type}
compress-level=${toString cfg.compression.level}
process-max=${toString cfg.processMax}
log-level-console=info
log-level-file=detail
log-path=/var/log/pgbackrest
spool-path=/var/spool/pgbackrest
[${cfg.stanza}]
pg1-path=/var/lib/postgresql/${config.services.postgresql.package.psqlSchema}
pg1-user=postgres
'';
};
systemd.services.pgbackrest-backup = {
description = "pgBackRest Full Backup";
after = ["postgresql.service" "pgbackrest-stanza-create.service"];
requires = ["postgresql.service"];
wants = ["pgbackrest-stanza-create.service"];
path = [pkgs.pgbackrest];
serviceConfig = {
Type = "oneshot";
User = "postgres";
EnvironmentFile = cfg.secretFile;
systemd.services.pgbackrest-stanza-create = {
description = "pgBackRest Stanza Create";
after = ["postgresql.service"];
requires = ["postgresql.service"];
path = [pkgs.pgbackrest];
serviceConfig = {
Type = "oneshot";
User = "postgres";
EnvironmentFile = cfg.secretFile;
RemainAfterExit = true;
};
script = ''
pgbackrest --stanza=${cfg.stanza} stanza-create || true
'';
};
script = ''
pgbackrest --stanza=${cfg.stanza} backup --type=full
'';
};
systemd.timers.pgbackrest-backup = {
wantedBy = ["timers.target"];
timerConfig = {
OnCalendar = cfg.schedule.full;
Persistent = true;
RandomizedDelaySec = "1h";
systemd.services.pgbackrest-backup = {
description = "pgBackRest Full Backup";
after = ["postgresql.service" "pgbackrest-stanza-create.service"];
requires = ["postgresql.service"];
wants = ["pgbackrest-stanza-create.service"];
path = [pkgs.pgbackrest];
serviceConfig = {
Type = "oneshot";
User = "postgres";
EnvironmentFile = cfg.secretFile;
};
script = ''
pgbackrest --stanza=${cfg.stanza} backup --type=full
'';
};
};
systemd.services.pgbackrest-backup-diff = {
description = "pgBackRest Differential Backup";
after = ["postgresql.service" "pgbackrest-stanza-create.service"];
requires = ["postgresql.service"];
wants = ["pgbackrest-stanza-create.service"];
path = [pkgs.pgbackrest];
serviceConfig = {
Type = "oneshot";
User = "postgres";
EnvironmentFile = cfg.secretFile;
systemd.timers.pgbackrest-backup = {
wantedBy = ["timers.target"];
timerConfig = {
OnCalendar = cfg.schedule.full;
Persistent = true;
RandomizedDelaySec = "1h";
};
};
script = ''
pgbackrest --stanza=${cfg.stanza} backup --type=diff
'';
};
systemd.timers.pgbackrest-backup-diff = {
wantedBy = ["timers.target"];
timerConfig = {
OnCalendar = cfg.schedule.diff;
Persistent = true;
RandomizedDelaySec = "5m";
systemd.services.pgbackrest-backup-diff = {
description = "pgBackRest Differential Backup";
after = ["postgresql.service" "pgbackrest-stanza-create.service"];
requires = ["postgresql.service"];
wants = ["pgbackrest-stanza-create.service"];
path = [pkgs.pgbackrest];
serviceConfig = {
Type = "oneshot";
User = "postgres";
EnvironmentFile = cfg.secretFile;
};
script = ''
pgbackrest --stanza=${cfg.stanza} backup --type=diff
'';
};
};
systemd.tmpfiles.rules = [
"d /var/lib/pgbackrest 0750 postgres postgres -"
"d /var/log/pgbackrest 0750 postgres postgres -"
"d /var/spool/pgbackrest 0750 postgres postgres -"
];
});
systemd.timers.pgbackrest-backup-diff = {
wantedBy = ["timers.target"];
timerConfig = {
OnCalendar = cfg.schedule.diff;
Persistent = true;
RandomizedDelaySec = "5m";
};
};
systemd.tmpfiles.rules = [
"d /var/lib/pgbackrest 0750 postgres postgres -"
"d /var/log/pgbackrest 0750 postgres postgres -"
"d /var/spool/pgbackrest 0750 postgres postgres -"
];
});
}