WIP: Add atomic backups with restic
This commit is contained in:
		
							parent
							
								
									ab5e088016
								
							
						
					
					
						commit
						729b442158
					
				
					 8 changed files with 263 additions and 3 deletions
				
			
		|  | @ -14,6 +14,7 @@ | |||
|     "${modulesPath}/profiles/minimal.nix" | ||||
|     (import ../modules) | ||||
| 
 | ||||
|     ./services/backups.nix | ||||
|     ./services/conduit.nix | ||||
|     ./services/foundryvtt.nix | ||||
|     ./services/gitea.nix | ||||
|  |  | |||
							
								
								
									
										180
									
								
								configuration/services/backups.nix
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										180
									
								
								configuration/services/backups.nix
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,180 @@ | |||
| { | ||||
|   config, | ||||
|   pkgs, | ||||
|   lib, | ||||
|   ... | ||||
| }: let | ||||
|   inherit (lib) types optional singleton; | ||||
|   mkShutdownScript = service: | ||||
|     pkgs.writeShellScript "backup-${service}-shutdown" '' | ||||
|       if systemctl is-active --quiet '${service}'; then | ||||
|         touch '/tmp/${service}-was-active' | ||||
|         systemctl stop '${service}' | ||||
|       fi | ||||
|     ''; | ||||
|   mkRestartScript = service: | ||||
|     pkgs.writeShellScript "backup-${service}-restart" '' | ||||
|       if [ -f '/tmp/${service}-was-active' ]; then | ||||
|         rm '/tmp/${service}-was-active' | ||||
|         systemctl start '${service}' | ||||
|       fi | ||||
|     ''; | ||||
|   writeScript = name: packages: text: | ||||
|     lib.getExe (pkgs.writeShellApplication { | ||||
|       inherit name text; | ||||
|       runtimeInputs = packages; | ||||
|     }); | ||||
| in { | ||||
|   options = { | ||||
|     services.backups = lib.mkOption { | ||||
|       description = lib.mdDoc '' | ||||
|         Configure restic backups with a specific tag. | ||||
|       ''; | ||||
|       type = types.attrsOf (types.submodule ({ | ||||
|         config, | ||||
|         name, | ||||
|         ... | ||||
|       }: { | ||||
|         options = { | ||||
|           user = lib.mkOption { | ||||
|             type = types.str; | ||||
|             description = '' | ||||
|               The user as which to run the backup. | ||||
|             ''; | ||||
|           }; | ||||
|           paths = lib.mkOption { | ||||
|             type = types.listOf types.str; | ||||
|             description = '' | ||||
|               The paths to back up. | ||||
|             ''; | ||||
|           }; | ||||
|           tag = lib.mkOption { | ||||
|             type = types.str; | ||||
|             description = '' | ||||
|               The restic tag to mark the backup with. | ||||
|             ''; | ||||
|             default = name; | ||||
|           }; | ||||
|           preparation = { | ||||
|             packages = lib.mkOption { | ||||
|               type = types.listOf types.package; | ||||
|               default = []; | ||||
|               description = '' | ||||
|                 The list of packages to make available in the | ||||
|                 preparation script. | ||||
|               ''; | ||||
|             }; | ||||
|             text = lib.mkOption { | ||||
|               type = types.nullOr types.str; | ||||
|               default = null; | ||||
|               description = '' | ||||
|                 The preparation script to run before the backup. | ||||
| 
 | ||||
|                 This should include things like database dumps and | ||||
|                 enabling maintenance modes. If a service needs to be | ||||
|                 shut down for backups, use `pauseServices` instead. | ||||
|               ''; | ||||
|             }; | ||||
|           }; | ||||
|           cleanup = { | ||||
|             packages = lib.mkOption { | ||||
|               type = types.listOf types.package; | ||||
|               default = []; | ||||
|               description = '' | ||||
|                 The list of packages to make available in the | ||||
|                 cleanup script. | ||||
|               ''; | ||||
|             }; | ||||
|             text = lib.mkOption { | ||||
|               type = types.nullOr types.str; | ||||
|               default = null; | ||||
|               description = '' | ||||
|                 The cleanup script to run after the backup. | ||||
| 
 | ||||
|                 This should do things like cleaning up database dumps | ||||
|                 and disabling maintenance modes. | ||||
|               ''; | ||||
|             }; | ||||
|           }; | ||||
|           pauseServices = lib.mkOption { | ||||
|             type = types.listOf types.str; | ||||
|             default = []; | ||||
|             description = '' | ||||
|               The systemd services that need to be shut down before | ||||
|               the backup can run. Services will be restarted after the | ||||
|               backup is complete. | ||||
| 
 | ||||
|               This is intended to be used for services that do not | ||||
|               support hot backups. | ||||
|             ''; | ||||
|           }; | ||||
|         }; | ||||
|       })); | ||||
|     }; | ||||
|   }; | ||||
| 
 | ||||
|   config = lib.mkIf (config.services.backups != {}) { | ||||
|     systemd.services = lib.mapAttrs' (name: backup: | ||||
|       lib.nameValuePair "backup-${name}" { | ||||
|         # Don't want to restart mid-backup | ||||
|         restartIfChanged = false; | ||||
| 
 | ||||
|         environment = { | ||||
|           RESTIC_CACHE_DIR = "%C/backup-${name}"; | ||||
|           RESTIC_PASSWORD_FILE = config.sops.secrets."restic/local-backups".path; | ||||
|           # TODO(tlater): If I ever add more than one repo, service | ||||
|           # shutdown/restarting will potentially break if multiple | ||||
|           # backups for the same service overlap. A more clever | ||||
|           # sentinel file with reference counts would probably solve | ||||
|           # this. | ||||
|           RESTIC_REPOSITORY = "/var/lib/backups/"; | ||||
|         }; | ||||
| 
 | ||||
|         serviceConfig = { | ||||
|           User = backup.user; | ||||
|           Group = "backup"; | ||||
|           RuntimeDirectory = "backup-${name}"; | ||||
|           CacheDirectory = "backup-${name}"; | ||||
|           CacheDirectoryMode = "0700"; | ||||
|           PrivateTmp = true; | ||||
| 
 | ||||
|           ExecStart = [ | ||||
|             (lib.concatStringsSep " " (["${pkgs.restic}/bin/restic" "backup" "--tag" name] ++ backup.paths)) | ||||
|           ]; | ||||
| 
 | ||||
|           ExecStartPre = | ||||
|             map (service: "+${mkShutdownScript service}") backup.pauseServices | ||||
|             ++ singleton (writeScript "backup-${name}-repo-init" [pkgs.restic pkgs.coreutils] '' | ||||
|               restic snapshots || (restic init && chmod -R g+rwx "$RESTIC_REPOSITORY"/*) | ||||
|             '') | ||||
|             ++ optional (backup.preparation.text != null) | ||||
|             (writeScript "backup-${name}-prepare" backup.preparation.packages backup.preparation.text); | ||||
| 
 | ||||
|           # TODO(tlater): Add repo pruning/checking | ||||
|           ExecStopPost = | ||||
|             map (service: "+${mkRestartScript service}") backup.pauseServices | ||||
|             ++ optional (backup.cleanup.text != null) | ||||
|             (writeScript "backup-${name}-cleanup" backup.cleanup.packages backup.cleanup.text); | ||||
|         }; | ||||
|       }) | ||||
|     config.services.backups; | ||||
| 
 | ||||
|     systemd.timers = lib.mapAttrs' (name: backup: | ||||
|       lib.nameValuePair "backup-${name}" { | ||||
|         wantedBy = ["timers.target"]; | ||||
|         timerConfig = { | ||||
|           OnCalendar = "*-*-* 02:30:00 UTC"; | ||||
|           RandomizedDelaySec = "1h"; | ||||
|           FixedRandomDelay = true; | ||||
|           Persistent = true; | ||||
|         }; | ||||
|       }) | ||||
|     config.services.backups; | ||||
| 
 | ||||
|     users.groups.backup = {}; | ||||
| 
 | ||||
|     systemd.tmpfiles.rules = [ | ||||
|       "d /var/lib/backups/ 0770 root backup" | ||||
|     ]; | ||||
|   }; | ||||
| } | ||||
|  | @ -231,4 +231,14 @@ in { | |||
|       }; | ||||
|     }; | ||||
|   }; | ||||
| 
 | ||||
|   services.backups.conduit = { | ||||
|     user = "root"; | ||||
|     paths = [ | ||||
|       "/var/lib/matrix-conduit/" | ||||
|     ]; | ||||
|     # Other services store their data in conduit, so no other services | ||||
|     # need to be shut down currently. | ||||
|     pauseServices = ["conduit.service"]; | ||||
|   }; | ||||
| } | ||||
|  |  | |||
|  | @ -1,4 +1,8 @@ | |||
| {config, ...}: let | ||||
| { | ||||
|   pkgs, | ||||
|   config, | ||||
|   ... | ||||
| }: let | ||||
|   domain = "gitea.${config.services.nginx.domain}"; | ||||
| in { | ||||
|   services.gitea = { | ||||
|  | @ -52,4 +56,24 @@ in { | |||
|       enabled = true | ||||
|     ''; | ||||
|   }; | ||||
| 
 | ||||
|   services.backups.gitea = { | ||||
|     user = "gitea"; | ||||
|     paths = [ | ||||
|       "/var/lib/gitea/gitea-db.sql" | ||||
|       "/var/lib/gitea/repositories/" | ||||
|       "/var/lib/gitea/data/" | ||||
|       "/var/lib/gitea/custom/" | ||||
|       # Conf is backed up via nix | ||||
|     ]; | ||||
|     preparation = { | ||||
|       packages = [config.services.postgresql.package]; | ||||
|       text = "pg_dump ${config.services.gitea.database.name} --file=/var/lib/gitea/gitea-db.sql"; | ||||
|     }; | ||||
|     cleanup = { | ||||
|       packages = [pkgs.coreutils]; | ||||
|       text = "rm /var/lib/gitea/gitea-db.sql"; | ||||
|     }; | ||||
|     pauseServices = ["gitea.service"]; | ||||
|   }; | ||||
| } | ||||
|  |  | |||
|  | @ -74,4 +74,33 @@ in { | |||
|       bantime = 86400 | ||||
|     ''; | ||||
|   }; | ||||
| 
 | ||||
|   services.backups.nextcloud = { | ||||
|     user = "nextcloud"; | ||||
|     paths = [ | ||||
|       "/var/lib/nextcloud/nextcloud-db.sql" | ||||
|       "/var/lib/nextcloud/data/" | ||||
|       "/var/lib/nextcloud/config/config.php" | ||||
|     ]; | ||||
|     preparation = { | ||||
|       packages = [ | ||||
|         config.services.postgresql.package | ||||
|         config.services.nextcloud.occ | ||||
|       ]; | ||||
|       text = '' | ||||
|         nextcloud-occ maintenance:mode --on | ||||
|         pg_dump ${config.services.nextcloud.config.dbname} --file=/var/lib/nextcloud/nextcloud-db.sql | ||||
|       ''; | ||||
|     }; | ||||
|     cleanup = { | ||||
|       packages = [ | ||||
|         pkgs.coreutils | ||||
|         config.services.nextcloud.occ | ||||
|       ]; | ||||
|       text = '' | ||||
|         rm /var/lib/nextcloud/nextcloud-db.sql | ||||
|         nextcloud-occ maintenance:mode --off | ||||
|       ''; | ||||
|     }; | ||||
|   }; | ||||
| } | ||||
|  |  | |||
|  | @ -110,4 +110,12 @@ in { | |||
|       # ProtectHome = "read-only"; # See further up | ||||
|     }; | ||||
|   }; | ||||
| 
 | ||||
|   services.backups.starbound = { | ||||
|     user = "root"; | ||||
|     paths = [ | ||||
|       "/var/lib/starbound/storage/universe/" | ||||
|     ]; | ||||
|     pauseServices = ["starbound.service"]; | ||||
|   }; | ||||
| } | ||||
|  |  | |||
|  | @ -19,6 +19,12 @@ | |||
|         mode = "0440"; | ||||
|       }; | ||||
| 
 | ||||
|       "restic/local-backups" = { | ||||
|         owner = "root"; | ||||
|         group = "backup"; | ||||
|         mode = "0440"; | ||||
|       }; | ||||
| 
 | ||||
|       "turn/env" = {}; | ||||
|       "turn/secret" = { | ||||
|         owner = "turnserver"; | ||||
|  |  | |||
|  | @ -7,6 +7,8 @@ heisenbridge: | |||
|     hs-token: ENC[AES256_GCM,data:VBwvwomv0Xg=,iv:q6INtJ+rg+QiXj8uBdBzQYQZUBBXp+9odxDHwvu8Jxc=,tag:XKhm8nxygAkKaiVPJ2Fcdg==,type:str] | ||||
| wireguard: | ||||
|     server-key: ENC[AES256_GCM,data:FvY897XdKoa/mckE8JQLCkklsnYD6Wz1wpsu5t3uhEnW3iarnDQxF9msuYU=,iv:jqGXfekM+Vs+J9b5nlZ5Skd1ZKHajoUo2Dc4tMYPm1w=,tag:EehikjI/FCU8wqtpvJRamQ==,type:str] | ||||
| restic: | ||||
|     local-backups: ENC[AES256_GCM,data:3QjEv03t7wE=,iv:y/6Lv4eUbZZfGPwUONykz8VNL62cAJuWaJy9yk3aAmk=,tag:wMlGsepuG9JjwtUKGWSibw==,type:str] | ||||
| turn: | ||||
|     env: ENC[AES256_GCM,data:xjIz/AY109lyiL5N01p5T3HcYco/rM5CJSRTtg==,iv:16bW6OpyOK/QL0QPGQp/Baa9xyT8E3ZsYkwqmjuofk0=,tag:J5re3uKxIykw3YunvQWBgg==,type:str] | ||||
|     secret: ENC[AES256_GCM,data:eQ7dAocoZtg=,iv:fgzjTPv30WqTKlLy+yMn5MsKQgjhPnwlGFFwYEg3gWs=,tag:1ze33U1NBkgMX/9SiaBNQg==,type:str] | ||||
|  | @ -19,8 +21,8 @@ sops: | |||
|     azure_kv: [] | ||||
|     hc_vault: [] | ||||
|     age: [] | ||||
|     lastmodified: "2023-04-23T17:35:16Z" | ||||
|     mac: ENC[AES256_GCM,data:4cW8k6o3jET8k+yJGyApjOyuSUQb+d+4wX/RTNnpbt+867sExQrZUrOMif/u8S4WmcKVSJgvrzuxK9hpDPYhJ1d/5YuHH1Dyj7QDRdhbZYHhkpPus0ZVTEpSknZzx2eWH1ch/fyJJknlrBlfb/tz50Dv+w9mhkL7qteaIq+Vmsc=,iv:YMfAuGwu1kAM0wGkq3kzVMnC72yo7ZT04BuEwoLRPIA=,tag:6I1VRzteRaLuxN+sfLA5Mw==,type:str] | ||||
|     lastmodified: "2023-09-22T21:07:02Z" | ||||
|     mac: ENC[AES256_GCM,data:gItC41S8MInLmikdH1okhPs+FVf8sCF/iQeJ5reigBunHkOngoc6nOFANyAcNZETszzhgTLXXtmVNEjW46v6K7D6nmoi/zwpedUxwzMwDC5I28VTMDHVMAThYSGtdo6kig8i2pi8rzEQd1DStxMv3TWML5y6DDTlFsd3lfudaHA=,iv:zXebvIVPR76GwUhpactwRgF/eEmx2OBkT18E8lkwzRA=,tag:6HyISACbFCGlpIIgkFeA/A==,type:str] | ||||
|     pgp: | ||||
|         - created_at: "2022-10-12T16:48:23Z" | ||||
|           enc: | | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue