52 Commits

Author SHA1 Message Date
3wc
7f1a02523e chore: publish 0.2.0+1.0.0 release 2023-11-10 14:15:58 +00:00
3wc
b01ad30ea0 Set manual tag 2023-11-10 14:13:51 +00:00
3wc
5e1032682b Switch to publishing on gitea 2023-11-10 14:12:32 +00:00
3wc
451c511554 Hopefully fix REMOVE_BACKUP_VOLUME_AFTER_UPLOAD 2023-09-28 10:18:18 +01:00
87d584e4e8 REALLY disable shellcheck 2023-09-26 16:48:29 +02:00
a171d9eea0 disable shellcheck 2023-09-26 16:45:58 +02:00
620ab4e3d7 add to .envrc.sample 2023-09-26 16:43:57 +02:00
3wc
83a3d82ea5 More HTTPS fixes 2023-09-19 15:45:37 +01:00
3wc
6450c80236 Add more HTTPS support 2023-09-19 15:40:20 +01:00
3wc
6f6a82153a Add HTTPS storage support 2023-09-19 15:39:56 +01:00
efc942c041 chore(deps): update docker docker tag to v24.0.6 2023-09-06 07:03:13 +00:00
0c4bc19e2a chore(deps): update docker docker tag to v24.0.5 2023-07-25 07:07:04 +00:00
dde9987de6 chore(deps): update docker docker tag to v24.0.4 2023-07-11 07:02:51 +00:00
5f734bc371 chore(deps): update docker docker tag to v24.0.3 2023-07-07 07:03:08 +00:00
27e2e61d7f chore(deps): update docker docker tag to v24.0.2 2023-05-29 07:03:02 +00:00
1bb1917e18 Merge pull request 'chore(deps): update docker docker tag to v24 (main)' (#14) from renovate/main-docker-24.x into main
Reviewed-on: coop-cloud/backup-bot-two#14
2023-05-28 14:23:14 +00:00
7b8b3b1acd chore(deps): update docker docker tag to v24 2023-05-22 07:06:36 +00:00
9c5ba87232 chore(deps): update docker docker tag to v23.0.6 2023-05-10 07:02:21 +00:00
9064bebb56 chore(deps): update docker docker tag to v23.0.5 2023-04-27 07:06:03 +00:00
4fdb585825 Merge pull request 'chore(deps): update docker docker tag to v23 (main)' (#11) from renovate/main-docker-23.x into main
Reviewed-on: coop-cloud/backup-bot-two#11
2023-04-24 08:17:36 +00:00
bde63b3f6f chore(deps): update docker docker tag to v23 2023-04-18 07:02:30 +00:00
92dfd23b26 feat: backupvolume can be pruned after upload 2023-03-01 13:29:00 +01:00
bab224ab96 chore(deps): update docker docker tag to v19.03.15 2023-01-19 08:03:49 +00:00
36928c34ac Merge pull request 'Configure Renovate' (#8) from renovate/configure into main
Reviewed-on: coop-cloud/backup-bot-two#8
2023-01-18 17:37:22 +00:00
9b324476c2 chore(deps): add renovate.json 2023-01-18 17:24:09 +00:00
7aa464e271 chore: publish 0.1.0+latest release 2022-10-11 15:28:50 +02:00
59c071734a add labels to get around abra checks 2022-10-11 15:24:35 +02:00
940b6bde1a Merge pull request 'backup multiple paths' (#5) from multi_path into main
Reviewed-on: coop-cloud/backup-bot-two#5
2021-12-14 14:44:05 +00:00
d6faffcbbd move rm up, to keep the latest backup in the volume 2021-12-13 11:37:52 +01:00
5a20ef4349 Merge branch 'main' into multi_path 2021-11-24 10:29:27 +00:00
ce42fb06fd fix docker cp paths 2021-11-24 11:17:13 +01:00
f2472bd0d3 make backup.path comma separated list 2021-11-23 17:38:31 +01:00
3wc
f7cbbf04c0 Goodbye, emojis! 😢
[ci skip]
2021-11-23 12:19:04 +02:00
3wc
ab03d2d7cc Ignore testing folder 2021-11-22 14:25:56 +02:00
3wc
32ba0041d1 chore: fix README bullet formatting
[ci skip]
2021-11-22 13:42:03 +02:00
3wc
394cc4f47c Mass README update
[ci skip]
2021-11-21 21:31:24 +02:00
53c4f1956a fix push docker image to correct destination 2021-11-16 12:14:21 +01:00
c750d6402f fix s3 restic_repo 2021-11-16 11:44:10 +01:00
b2e2fc9d13 fix typo 2021-11-16 11:43:47 +01:00
3wc
9e818ed021 Revert to previous, probably-working cron set-up 2021-11-11 12:05:04 +02:00
3wc
f6d1da8899 Working cron again, d'oh 2021-11-11 02:10:17 +02:00
3wc
d3e9001597 Add badge, tidy docs 2021-11-11 00:18:50 +02:00
3wc
f7db376377 Appease shellcheck 2021-11-11 00:02:44 +02:00
3wc
d6e90e04ba SSH_HOST_KEY_DISABLE, add drone pipline 2021-11-11 00:00:39 +02:00
3wc
a990dc27c7 SSH host keys, split out swarm-cronjob 2021-11-10 22:02:13 +02:00
3wc
721c393d2d Allow overriding cron schedule, fix vars 2021-11-10 21:17:12 +02:00
3wc
c9de239e93 Bash command line handling showdown 2021-11-09 15:25:43 +02:00
3wc
489ef570dd Fix AWS S3 settings 2021-11-09 14:30:19 +02:00
3wc
23b092776f More progress towards S3/SSH 2021-11-09 14:20:11 +02:00
3wc
ed76e6164b Work-in-progress: split S3 & SSH storage 2021-11-09 12:37:56 +02:00
3wc
f5e87f396a Update TODO list and tidy up README 2021-11-09 12:27:54 +02:00
3wc
8317f50a8a Variables, Dockerfile, better syntax, etc. 2021-11-06 19:45:39 +02:00
14 changed files with 398 additions and 69 deletions

44
.drone.yml Normal file
View File

@ -0,0 +1,44 @@
---
kind: pipeline
name: linters
steps:
- name: run shellcheck
image: koalaman/shellcheck-alpine
commands:
- shellcheck backup.sh
- name: publish image
image: plugins/docker
settings:
username: 3wordchant
password:
from_secret: git_coopcloud_tech_token_3wc
repo: git.coopcloud.tech/coop-cloud/backup-bot-two
tags: 1.0.0
registry: git.coopcloud.tech
depends_on:
- run shellcheck
when:
event:
exclude:
- pull_request
trigger:
branch:
- bb2-classic
---
kind: pipeline
name: generate recipe catalogue
steps:
- name: release a new version
image: plugins/downstream
settings:
server: https://build.coopcloud.tech
token:
from_secret: drone_abra-bot_token
fork: true
repositories:
- coop-cloud/auto-recipes-catalogue-json
trigger:
event: tag

29
.env.sample Normal file
View File

@ -0,0 +1,29 @@
TYPE=backup-bot-two
SECRET_RESTIC_PASSWORD_VERSION=v1
COMPOSE_FILE=compose.yml
SERVER_NAME=example.com
RESTIC_HOST=minio.example.com
CRON_SCHEDULE='*/5 * * * *'
REMOVE_BACKUP_VOLUME_AFTER_UPLOAD=1
# swarm-cronjob, instead of built-in cron
#COMPOSE_FILE="$COMPOSE_FILE:compose.swarm-cronjob.yml"
# SSH storage
#SECRET_SSH_KEY_VERSION=v1
#SSH_HOST_KEY="hostname ssh-rsa AAAAB3...
#COMPOSE_FILE="$COMPOSE_FILE:compose.ssh.yml"
# S3 storage
#SECRET_AWS_SECRET_ACCESS_KEY_VERSION=v1
#AWS_ACCESS_KEY_ID=something-secret
#COMPOSE_FILE="$COMPOSE_FILE:compose.s3.yml"
# HTTPS storage
#SECRET_HTTPS_PASSWORD_VERSION=v1
#COMPOSE_FILE="$COMPOSE_FILE:compose.https.yml"
#RESTIC_USER=<somebody>

17
.envrc.sample Normal file
View File

@ -0,0 +1,17 @@
export RESTIC_HOST="user@domain.tld"
export RESTIC_PASSWORD_FILE=/run/secrets/restic-password
export BACKUP_DEST=/backups
export SERVER_NAME=domain.tld
export DOCKER_CONTEXT=$SERVER_NAME
# uncomment either this:
#export SSH_KEY_FILE=~/.ssh/id_rsa
# or this:
#export AWS_SECRET_ACCESS_KEY_FILE=s3
#export AWS_ACCESS_KEY_ID=easter-october-emphatic-tug-urgent-customer
# or this:
#export HTTPS_PASSWORD_FILE=/run/secrets/https_password
# optionally limit subset of services for testing
#export SERVICES_OVERRIDE="ghost_domain_tld_app ghost_domain_tld_db"

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/testing

13
Dockerfile Normal file
View File

@ -0,0 +1,13 @@
FROM docker:24.0.6-dind
RUN apk add --upgrade --no-cache \
bash \
curl \
jq \
restic
COPY backup.sh /usr/bin/backup.sh
COPY setup-cron.sh /usr/bin/setup-cron.sh
RUN chmod +x /usr/bin/backup.sh /usr/bin/setup-cron.sh
ENTRYPOINT [ "/usr/bin/setup-cron.sh" ]

View File

@ -1,45 +1,72 @@
# Backupbot II: This Time It's Easily Configurable
# Backupbot II
Automatically backup files from running Docker Swarm services based on labels.
[![Build Status](https://build.coopcloud.tech/api/badges/coop-cloud/backup-bot-two/status.svg)](https://build.coopcloud.tech/coop-cloud/backup-bot-two)
## TODO
_This Time, It's Easily Configurable_
- [ ] Make a Docker image of this
- [ ] Rip out or improve Restic stuff
- [ ] Add secret handling for database backups
- [ ] Continuous linting with shellcheck
Automatically take backups from running Docker Swarm services into a volume.
## Label format
## Background
(Haven't done secrets yet, here are two options)
There are lots of Docker volume backup systems; all of them have one or both of these limitations:
- You need to define all the volumes to back up in the configuration system
- Backups require services to be stopped to take consistent copies
Backupbot II tries to help, by
1. **letting you define backups using Docker labels**, so you can **easily collect your backups for use with another system** like docker-volume-backup.
2. **running pre- and post-commands** before and after backups, for example to use database tools to take a backup from a running service.
## Deployment
### With Co-op Cloud
1. Set up Docker Swarm and [`abra`][abra]
2. `abra app new backup-bot-two`
3. `abra app config <your-app-name>`, and set storage options. Either configure `CRON_SCHEDULE`, or set up `swarm-cronjob`
4. `abra app secret generate <your-app-name> restic-password v1`, optionally with `--pass` before `<your-app-name>` to save the generated secret in `pass`.
5. `abra app secret insert <your-app-name> ssh-key v1 ...` or similar, to load required secrets.
4. `abra app deploy <your-app-name>`
<!-- metadata -->
* **Category**: Utilities
* **Status**: 0, work-in-progress
* **Image**: [`thecoopcloud/backup-bot-two`](https://hub.docker.com/r/thecoopcloud/backup-bot-two), 4, upstream
* **Healthcheck**: No
* **Backups**: N/A
* **Email**: N/A
* **Tests**: No
* **SSO**: N/A
<!-- endmetadata -->
## Configuration
Like Traefik, or `swarm-cronjob`, Backupbot II uses access to the Docker socket to read labels from running Docker Swarm services:
v1:
```
services:
db:
deploy:
labels:
backupbot.backup: "true"
backupbot.backup.repos: "$some_thing"
backupbot.backup.at: "* * * * *"
backupbot.backup.pre-hook: 'mysqldump -u root -p"$(cat /run/secrets/db_root_password)" -f /tmp/dump/dump.db'
backupbot.backup.post-hook: "rm -rf /tmp/dump/dump.db"
backupbot.backup.path: "/tmp/dump/"
```
v2:
```
deploy:
labels:
backupbot.backup: "true"
backupbot.backup.repos: "$some_thing"
backupbot.backup.at: "* * * * *"
backupbot.backup.post-hook: "rm -rf /tmp/dump/dump.db"
backupbot.backup.secrets": "db_root_password",
backupbot.backup.pre-hook: 'mysqldump -u root -p"$DB_ROOT_PASSWORD" -f /tmp/dump/dump.db'
backupbot.backup.path: "/tmp/dump/,/etc/foo/"
```
## Questions:
- `backupbot.backup` -- set to `true` to back up this service (REQUIRED)
- `backupbot.backup.path` -- comma separated list of file paths within the service to copy (REQUIRED)
- `backupbot.backup.pre-hook` -- command to run before copying files (optional)
- `backupbot.backup.post-hook` -- command to run after copying files (optional)
- Should frequency be configurable per service, centrally, or both?
As in the above example, you can reference Docker Secrets, e.g. for looking up database passwords, by reading the files in `/run/secrets` directly.
```
- "backupbot.backup.at: "* * * * *"
```
## Development
1. Install `direnv`
2. `cp .envrc.sample .envrc`
3. Edit `.envrc` as appropriate, including setting `DOCKER_CONTEXT` to a remote Docker context, if you're not running a swarm server locally.
4. Run `./backup.sh` -- you can add the `--skip-backup` or `--skip-upload` options if you just want to test one other step
[abra]: https://git.autonomic.zone/autonomic-cooperative/abra

169
backup.sh
View File

@ -1,50 +1,139 @@
#!/bin/bash
# FIXME: just for testing
backup_path=backups
server_name="${SERVER_NAME:?SERVER_NAME not set}"
# FIXME: just for testing
export DOCKER_CONTEXT=demo.coopcloud.tech
restic_password_file="${RESTIC_PASSWORD_FILE:?RESTIC_PASSWORD_FILE not set}"
mapfile -t services < <(docker service ls --format '{{ .Name }}')
restic_host="${RESTIC_HOST:?RESTIC_HOST not set}"
# FIXME: just for testing
services=( "ghost_demo_app" "ghost_demo_db" )
backup_path="${BACKUP_DEST:?BACKUP_DEST not set}"
for service in "${services[@]}"; do
echo "service: $service"
details=$(docker service inspect "$service" --format "{{ json .Spec.Labels }}")
if echo "$details" | jq -r '.["backupbot.backup"]' | grep -q 'true'; then
pre=$(echo "$details" | jq -r '.["backupbot.backup.pre-hook"]')
post=$(echo "$details" | jq -r '.["backupbot.backup.post-hook"]')
path=$(echo "$details" | jq -r '.["backupbot.backup.path"]')
if [ "$path" = "null" ]; then
echo "ERROR: missing 'path' for $service"
continue # or maybe exit?
fi
# shellcheck disable=SC2153
ssh_key_file="${SSH_KEY_FILE}"
s3_key_file="${AWS_SECRET_ACCESS_KEY_FILE}"
# shellcheck disable=SC2153
https_password_file="${HTTPS_PASSWORD_FILE}"
container=$(docker container ls -f "name=$service" --format '{{ .ID }}')
echo "backing up $service"
test -d "$backup_path/$service" || mkdir "$backup_path/$service"
if [ "$pre" != "null" ]; then
# run the precommand
# shellcheck disable=SC2086
docker exec "$container" $pre
fi
restic_repo=
restic_extra_options=
# run the backup
docker cp "$container:$path" "$backup_path/$service"
if [ -n "$ssh_key_file" ] && [ -f "$ssh_key_file" ]; then
restic_repo="sftp:$restic_host:/$server_name"
if [ "$post" != "null" ]; then
# run the postcommand
# shellcheck disable=SC2086
docker exec "$container" $post
fi
# Only check server against provided SSH_HOST_KEY, if set
if [ -n "$SSH_HOST_KEY" ]; then
tmpfile=$(mktemp)
echo "$SSH_HOST_KEY" >>"$tmpfile"
echo "using host key $SSH_HOST_KEY"
ssh_options="-o 'UserKnownHostsFile $tmpfile'"
elif [ "$SSH_HOST_KEY_DISABLE" = "1" ]; then
echo "disabling SSH host key checking"
ssh_options="-o 'StrictHostKeyChecking=No'"
else
echo "neither SSH_HOST_KEY nor SSH_HOST_KEY_DISABLE set"
fi
restic -p restic-password \
backup --quiet -r sftp:u272979@u272979.your-storagebox.de:/demo.coopcloud.tech \
--tag coop-cloud "$backup_path"
done
restic_extra_options="sftp.command=ssh $ssh_options -i $ssh_key_file $restic_host -s sftp"
fi
if [ -n "$s3_key_file" ] && [ -f "$s3_key_file" ] && [ -n "$AWS_ACCESS_KEY_ID" ]; then
AWS_SECRET_ACCESS_KEY="$(cat "${s3_key_file}")"
export AWS_SECRET_ACCESS_KEY
restic_repo="s3:$restic_host:/$server_name"
fi
if [ -n "$https_password_file" ] && [ -f "$https_password_file" ]; then
HTTPS_PASSWORD="$(cat "${https_password_file}")"
export HTTPS_PASSWORD
restic_user="${RESTIC_USER:?RESTIC_USER not set}"
restic_repo="rest:https://$restic_user:$HTTPS_PASSWORD@$restic_host"
fi
if [ -z "$restic_repo" ]; then
echo "you must configure either SFTP, S3, or HTTPS storage, see README"
exit 1
fi
echo "restic_repo: $restic_repo"
# Pre-bake-in some default restic options
_restic() {
if [ -z "$restic_extra_options" ]; then
# shellcheck disable=SC2068
restic -p "$restic_password_file" \
--quiet -r "$restic_repo" \
$@
else
# shellcheck disable=SC2068
restic -p "$restic_password_file" \
--quiet -r "$restic_repo" \
-o "$restic_extra_options" \
$@
fi
}
if [ -n "$SERVICES_OVERRIDE" ]; then
# this is fine because docker service names should never include spaces or
# glob characters
# shellcheck disable=SC2206
services=($SERVICES_OVERRIDE)
else
mapfile -t services < <(docker service ls --format '{{ .Name }}')
fi
if [[ \ $*\ != *\ --skip-backup\ * ]]; then
rm -rf "${backup_path}"
for service in "${services[@]}"; do
echo "service: $service"
details=$(docker service inspect "$service" --format "{{ json .Spec.Labels }}")
if echo "$details" | jq -r '.["backupbot.backup"]' | grep -q 'true'; then
pre=$(echo "$details" | jq -r '.["backupbot.backup.pre-hook"]')
post=$(echo "$details" | jq -r '.["backupbot.backup.post-hook"]')
path=$(echo "$details" | jq -r '.["backupbot.backup.path"]')
if [ "$path" = "null" ]; then
echo "ERROR: missing 'path' for $service"
continue # or maybe exit?
fi
container=$(docker container ls -f "name=$service" --format '{{ .ID }}')
echo "backing up $service"
if [ "$pre" != "null" ]; then
# run the precommand
# shellcheck disable=SC2086
docker exec "$container" sh -c "$pre"
fi
# run the backup
for p in ${path//,/ }; do
# creates the parent folder, so `docker cp` has reliable behaviour no matter if $p ends with `/` or `/.`
dir=$backup_path/$service/$(dirname "$p")
test -d "$dir" || mkdir -p "$dir"
docker cp -a "$container:$p" "$dir/$(basename "$p")"
done
if [ "$post" != "null" ]; then
# run the postcommand
# shellcheck disable=SC2086
docker exec "$container" sh -c "$post"
fi
fi
done
# check if restic repo exists, initialise if not
if [ -z "$(_restic cat config)" ] 2>/dev/null; then
echo "initializing restic repo"
_restic init
fi
fi
if [[ \ $*\ != *\ --skip-upload\ * ]]; then
_restic backup --host "$server_name" --tag coop-cloud "$backup_path"
if [ "$REMOVE_BACKUP_VOLUME_AFTER_UPLOAD" -eq 1 ]; then
echo "Cleaning up ${backup_path}"
rm -rf "${backup_path:?}"/*
fi
fi

15
compose.https.yml Normal file
View File

@ -0,0 +1,15 @@
---
version: "3.8"
services:
app:
environment:
- HTTPS_PASSWORD_FILE=/run/secrets/https_password
- RESTIC_USER
secrets:
- source: https_password
mode: 0400
secrets:
https_password:
external: true
name: ${STACK_NAME}_https_password_${SECRET_HTTPS_PASSWORD_VERSION}

14
compose.s3.yml Normal file
View File

@ -0,0 +1,14 @@
---
version: "3.8"
services:
app:
environment:
- AWS_ACCESS_KEY_ID
- AWS_SECRET_ACCESS_KEY_FILE=/run/secrets/aws_secret_access_key
secrets:
- aws_secret_access_key
secrets:
aws_secret_access_key:
external: true
name: ${STACK_NAME}_aws_secret_access_key_${SECRET_AWS_SECRET_ACCESS_KEY_VERSION}

16
compose.ssh.yml Normal file
View File

@ -0,0 +1,16 @@
---
version: "3.8"
services:
app:
environment:
- SSH_KEY_FILE=/run/secrets/ssh_key
- SSH_HOST_KEY
- SSH_HOST_KEY_DISABLE
secrets:
- source: ssh_key
mode: 0400
secrets:
ssh_key:
external: true
name: ${STACK_NAME}_ssh_key_${SECRET_SSH_KEY_VERSION}

15
compose.swarm-cronjob.yml Normal file
View File

@ -0,0 +1,15 @@
---
version: "3.8"
services:
app:
deploy:
mode: replicated
replicas: 0
labels:
- "swarm.cronjob.enable=true"
# Note(3wc): every 5m, testing
- "swarm.cronjob.schedule=*/5 * * * *"
# Note(3wc): blank label to be picked up by `abra recipe sync`
restart_policy:
condition: none
entrypoint: [ "/usr/bin/backup.sh" ]

35
compose.yml Normal file
View File

@ -0,0 +1,35 @@
---
version: "3.8"
services:
app:
image: git.coopcloud.tech:1.0.0
# build: .
volumes:
- "/var/run/docker.sock:/var/run/docker.sock"
- "backups:/backups"
environment:
- CRON_SCHEDULE
- RESTIC_REPO
- RESTIC_PASSWORD_FILE=/run/secrets/restic_password
- BACKUP_DEST=/backups
- RESTIC_HOST
- SERVER_NAME
- REMOVE_BACKUP_VOLUME_AFTER_UPLOAD=1
secrets:
- restic_password
deploy:
labels:
- "traefik.enable=true"
- "traefik.http.services.${STACK_NAME}.loadbalancer.server.port=8008"
- "traefik.http.routers.${STACK_NAME}.rule="
- "traefik.http.routers.${STACK_NAME}.entrypoints=web-secure"
- "traefik.http.routers.${STACK_NAME}.tls.certresolver=${LETS_ENCRYPT_ENV}"
- coop-cloud.${STACK_NAME}.version=0.2.0+1.0.0
volumes:
backups:
secrets:
restic_password:
external: true
name: ${STACK_NAME}_restic_password_${SECRET_RESTIC_PASSWORD_VERSION}

3
renovate.json Normal file
View File

@ -0,0 +1,3 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
}

11
setup-cron.sh Normal file
View File

@ -0,0 +1,11 @@
#!/bin/bash
set -e
set -o pipefail
cron_schedule="${CRON_SCHEDULE:?CRON_SCHEDULE not set}"
echo "$cron_schedule /usr/bin/backup.sh" | crontab -
crontab -l
crond -f -d8 -L /dev/stdout