a friend of mine recently told me about a way of performing incremental backups making use of the ubiquitous
I was really impressed that such a useful thing could be hidden there without people talking about it (or is it just me who didn’t know? anyway …)
Here are my two cents on how you can tailor a script that will backup a directory incrementally sending the snapshots to S3 and then recovering later.
In the end, I also include
restic as an alternative.
The ultimate goal is pretty simple to describe:
- perform backups that we can go back to if we need and that we can store in a remote location;
With an extra feature that is very nice to have: restore the backups up to a specific date.
The idea of restoring to a point-in-time snapshot is that if we mess up and backup our mess we want to go back to a previous backup that wasn’t messed up.
Consider the following example: starting at
t0 we perform our initial snapshots that corresponds to an addition of all the present files and directories. Next, in
t1, we perform a second snapshot which accounts for only the addition of a new file. In a third time,
t2, we perform another snapshot which differs from the last one by only adding a new file. Now assume that at
t3 we perform a new snapshot but we shouldn’t have snapshotted as we added a wrong file (e.g, the database was already broken and our snapshotting routine took the snapshot).
If we can go back in time to any snapshot that we want (instead of relying on a single backup), then we can recover even from a bad snapshot taken at the wrong moment. In our example, that would mean not recovering to
t3, but instead, recover to
t2 where we took a good snapshot.
With that in hands, we can make sure that even if we wrongly backup a directory where things got removed we can still go to a snapshot taken previously and get to that state.
Install the dependencies -
If you’re using MacOS you can retrieve
brew as it already has a formula (see gnu-tar formula):
brew install gnu-tar gtar --help Usage: gtar [OPTION...] [FILE]... GNU 'tar' saves many files together into a single tape or disk archive, and can restore individual files from the archive. Examples: tar -cf archive.tar foo bar # Create archive.tar from files foo and bar.
If you don’t want to have to prefix with
g we can pass an additional argument,
--with-default-names as we can see in the formula (line 16 from
option "with-default-names", "Do not prepend 'g' to the binary" # ... test do tar = build.with?("default-names") ? bin/"tar" : bin/"gtar" # ... end
so that we install it with the following command:
brew install gnu-tar --with-default-names
I’ll stick with
gtar (installation with
--with-default-names). You can know whether you’re using the BSD or GNU version by looking at
tar --help ++==== BSD || \/ tar(bsdtar): manipulate archive files First option must be a mode specifier: -c Create -r Add/Replace -t List -u Update -x Extract ...
Cool, let’s jump to the backups.
Performing incremental backups
The manuals of
tar contain a very interesting section Using tar to perform incremental dumps that states:
The option `--listed-incremental' instructs tar to operate on an incremental archive with additional metadata stored in a standalone file, called a snapshot file. The purpose of this file is to help determine which files have been changed, added or deleted since the last backup, so that the next incremental backup will contain only modified files. The name of the snapshot file is given as an argument to the option: `--listed-incremental=file'
That essentially means that we can create some various
tar files and keep track of this sequence of files by mutating an index file that we provide to the
So, how to perform incremental backups using Tar?
- create a list that keeps track of all the files that compose the full state of the system up until a given point in time
- create the initial tar with the full current state
- keep creating files with only the difference (increments) and updating the file that lists these snapshots that we take.
As an example, assume we have the following file structure:
. ├── rootfs │ └── 1.txt └── snapshots 2 directories, 1 file
We want to back up
/rootfs and, which right now contains
1.txt, and send the snapshots to the
Starting with the first and second point (creating a list that keeps track of all the files that compose the full state and then creating the first snapshots that contain all the state):
gtar \ --create \ --no-check-device \ --file=./snapshots/1.tar \ --listed-incremental=./snapshots/index \ ./rootfs
which leds us to the following:
tree . ├── rootfs │ └── 1.txt └── snapshots ├── 1.tar └── index 2 directories, 3 files cat ./snapshots/index GNU tar-1.29-2 1511892293436890000151189201680544478167772208590981353./rootfsY1.txt%
Now mutate the state of
rootfs directory one more time by adding
3.txt and then perform a second snapshot:
# mutate the rootfs directory echo "2.txt" > ./rootfs/2.txt echo "3.txt" > ./rootfs/3.txt tree . ├── rootfs │ ├── 1.txt │ ├── 2.txt │ └── 3.txt └── snapshots ├── 1.tar └── index # perform a second snapshot that should # contain only the addition of the two files gtar \ --create \ --no-check-device \ --file=./snapshots/2.tar \ --listed-incremental=./snapshots/index \ ./rootfs # verify that the new snapshot has been created # but we still have a single `index` file that # lists the contents tree . ├── rootfs │ ├── 1.txt │ ├── 2.txt │ └── 3.txt └── snapshots ├── 1.tar ├── 2.tar └── index # check the updated index file cat ./snapshots/index GNU tar-1.29-2 151189253560533700001511892488471449116167772208590981353./rootfsN1.txtY2.txtY3.txt # check the contents of the first snapshot # (it should only contain 1.txt) tar -tvf ./snapshots/1.tar drwxr-xr-x 0 cirocosta wheel 8 Nov 28 16:00 ./rootfs/ -rw-r--r-- 0 cirocosta wheel 2 Nov 28 16:00 ./rootfs/1.txt # check the contents of the first snapshot # (it should only contain 2.txt and 3.txt) tar -tvf ./snapshots/2.tar drwxr-xr-x 0 cirocosta wheel 22 Nov 28 16:08 ./rootfs/ -rw-r--r-- 0 cirocosta wheel 6 Nov 28 16:08 ./rootfs/2.txt -rw-r--r-- 0 cirocosta wheel 6 Nov 28 16:08 ./rootfs/3.txt
Cool, having that all we need to do is restore to check if it’s really working.
# create a separate directory to put the # restoration files. This is important for # the demo because otherwise `gtar` would # remove the files from `rootfs` that were # not at the snapshot at that given moment. mkdir ./restore cd restore gtar \ --extract \ --listed-incremental=../snapshots/index \ --file=../snapshots/1.tar # check if only `1.txt` is there in the # rootfs from the first moment tree . └── rootfs └── 1.txt # remove the contents so that we start again # with a clean rootfs rm -rf ./rootfs # to restore all the files until the last snapshot # we must perform the extraction of all the snapshots # from the first to the last, one after another gtar \ --extract \ --listed-incremental=../snapshots/index \ --file=../snapshots/1.tar gtar \ --extract \ --listed-incremental=../snapshots/index \ --file=../snapshots/2.tar # now check that all the files of the last snapshot # are there tree . └── rootfs ├── 1.txt ├── 2.txt └── 3.txt 1 directory, 3 files
Integrating with S3
Someone who has worked with S3 knows that now syncing this to S3 is not a big deal - make use of the AWS CLI and then it’ll push the new / changed files to a bucket that you want.
Besides the simplicity of uploading content to S3, I’d like to highlight a simple way of testing it locally. The easiest way to do it, in my opinion, is using minio which provides us an implementation of the S3 API that we can use locally:
Minio is an open source object storage server with Amazon S3 compatible API. Build cloud-native applications portable across all major public and private clouds.
If you have Docker installed, having
minio running is one command away:
# Run a container in the background using the image # `minio/minio` publishing the target port 9000 as 9000 # on the host and initialize it with the command `server` # with the argument `/data` (location where minio # will save the contents - this is a directory you'd # like persisted somehow, here, we don't care). # With MINIO_ACCESS_KEY and MINIO_SECRET_KEY we set some # testing credentials that we can use with the AWS CLI tool. docker run \ --publish 9000:9000 \ --name minio \ --detach \ --env 'MINIO_ACCESS_KEY=accesskey' \ --env 'MINIO_SECRET_KEY=secretkey' \ minio/minio \ server /data
Now to make use of it we can install the AWS CLI and configure it:
# install `pip` to fetch python packages (in # this case, `awscli`) sudo easy_install pip # fetch the `awscli` package. pip install awscli --upgrade Configure the credentials: aws configure AWS Access Key ID [None]: accesskey AWS Secret Access Key [None]: secretkey Default region name [us-east-1]: Default output format [None]: # enable AWS signature V4 for the minio server aws configure set default.s3.signature_version s3v4 # make sure we don't have previous environment # variables set unset AWS_DEFAULT_REGION unset AWS_ACCESS_KEY_ID unset AWS_SECRET_ACCESS_KEY
With everything configured, we can create a bucket and then sync our snapshots:
aws \ --endpoint-url=http://localhost:9000 \ s3 mb \ s3://backups make_bucket: backups # get into the snapshots directory cd ./snapshots # Sync the current directory (snapshots) with # the S3 bucket. # Only new files will be sent on each new run of # the command. If there are no changes, nothing # is sent. # Because it's our first run, everything is sent. aws \ --endpoint-url=http://localhost:9000 \ s3 sync \ ./ s3://backups upload: ./2.tar to s3://backups/2.tar upload: ./1.tar to s3://backups/1.tar upload: ./index to s3://backups/index
Now, to retrieve all the files from that bucket:
# copy from a remote location (s3 bucket `backups`) # to the current location `./` recursively. aws \ --endpoint-url=http://localhost:9000 \ s3 cp \ s3://backups ./ \ --recursive download: s3://backups/index to ./index download: s3://backups/1.tar to ./1.tar download: s3://backups/2.tar to ./2.tar
Alternative - restic
In the search of a better UX for performing these incremental backups, I stumbled upon restic, a backup program in Go that takes care of sending the backups to a remote location and presents a delightful view of the snapshots that have been taken. Besides that, it also takes care of encrypting the contents and performing integrity checks, pretty cool.
To install it on macOS you can go to the restic/releases GitHub page and take the latest release (in my case, version 0.8.0) and then fetch the
.bz2 file, decompress it and put in your
# Fetch the .bz2 file with some options: # --show-error will fail with error messages # if something goes wrong # --remote-name instructs `curl` to save the # file with the name it receives # from the remote # --location follows redirects if there are # any curl \ --show-error \ --remote-name \ --location \ https://github.com/restic/restic/releases/download/v0.8.0/restic_0.8.0_darwin_amd64.bz2 # Decompress the file bzip2 \ --decompress \ ./restic_0.8.0_darwin_amd64.bz2 # Move it to somewhere in your $PATH sudo mv ./restic_0.8.0_darwin_amd64 /usr/local/bin/restic # Modify the permission bits (make it executable) sudo chmod +x /usr/local/bin/restic # Check if it worked restic --help
restic set up we can perform a local snapshot as we performed before with
# Create a snapshots directory that will # hold the repository structure that # restic uses to keep track of the changes # and store the snapshotted content. mkdir snapshots # Create a rootfs that will contain the data # that we plan to backup. mkdir rootfs echo "1" > rootfs/1.txt tree . ├── rootfs │ └── 1.txt └── snapshots # initialize the restic repository at `./snapshots` restic init --repo ./snapshots enter password for new backend: enter password again: created restic backend 100efe3e68 at ./snapshots # check how the filesystem looks like after # the initialization tree . ├── rootfs │ └── 1.txt └── snapshots ├── config ├── data │ ├── 00 │ ├── 01 ... │ └── ff ├── index ├── keys │ └── 04b12ce19886a5d25... ├── locks └── snapshots # Perform our first snapshot of `rootfs` # saving it in our repository that we # initialized before. restic --repo ./snapshots backup ./rootfs enter password for repository: password is correct scan [./rootfs] scanned 1 directories, 1 files in 0:00 [0:00] 100.00% 0B/s 2B / 2B 2 / 2 items 0 errors ETA 0:00 duration: 0:00, 0.00MiB/s snapshot 9171fe56 saved # With the snapshot creation succeeded, let's # check how the snapshots dir changed. tree . ├── rootfs │ └── 1.txt └── snapshots ├── config ├── data │ ├── 00 ... │ ├── 06 # some new stuff │ │ └── 0623cae88fefc31f07c09a11e8743b9eb94877883d458a6bcc2ec8a870bccf37 ... ├── index # index dir has some content │ └── 89cbbfae048c49f7abbaa5ac929971550537eb97195039f0a70220890b08d259 ... ... └── snapshots # snapshot! └── 9171fe56882a6ac7d4cc8836ec096dc761698dd2d47bfe8b0f9a6b82c061b7dc # Having the snapshot created we can # now create a `restored` directory where # we'll put the results of the restoration mkdir restored # Perform the restoration by pointing restic # at the snapshots directory and a target # directory that will receive the restored files. restic \ --repo ./snapshots \ restore \ --target ./restored \ latest enter password for repository: password is correct restoring <Snapshot 9171fe56 of [./rootfs] (...) to ./restored # Go to the `restored` directory ad check if it worked cd restored tree . └── rootfs └── 1.txt # We can also check what are the files in # a given snapshot as well as list all the # snapshots we've taken so far. # Let's first list the snapshots restic --repo ./snapshots snapshots enter password for repository: password is correct ID Date Host Tags Directory ---------------------------------------------------------------------- 9171fe56 2017-12-01 09:17:51 cirocosta.local ./rootfs ---------------------------------------------------------------------- 1 snapshots # With the ID of the snapshot we can then # inspect the contents of the snapshot restic \ --repo ./snapshots \ ls 9171fe56 enter password for repository: password is correct snapshot 9171fe56 of (...): /rootfs /rootfs/1.txt # Now, what if we create a new file, snapshot # it and then check the contents? # Create a new file: echo "2" > ./rootfs/2.txt # Take the new snapshot restic --repo ./snapshots backup ./rootfs enter password for repository: password is correct using parent snapshot 9171fe56 scan [./rootfs] scanned 1 directories, 2 files in 0:00 [0:00] 100.00% 0B/s 4B / 4B 3 / 3 items 0 errors ETA 0:00 duration: 0:00, 0.00MiB/s snapshot 3b5e8701 saved # Look at the snapshots that we created restic --repo ./snapshots snapshots ID Date Host Tags Directory ---------------------------------------------------------------------- 9171fe56 2017-12-01 09:17:51 cirocosta.local /tmp/cc/rootfs 3b5e8701 2017-12-01 10:38:12 cirocosta.local /tmp/cc/rootfs ---------------------------------------------------------------------- 2 snapshots # Check the state of the latest snapshot # (we `latest` is an alias for the last snapshot) restic --repo ./snapshots ls latest /rootfs /rootfs/1.txt /rootfs/2.txt # Interesting. Looking at the latest snapshot # reveals all the files, not only the addition # of `2` - it shows us the complete representation # of the final state. # If we look at the first snapshot though, # then it shows only the final state up to # that point in time. restic --repo ./snapshots ls 9171fe56 /rootfs /rootfs/1.txt
It works! We have our files there.
It’s important to note that right now
restic does not have support for compression but it performs deduplication across the backups and the backups are all incremental.
As it already has S3 support we can test it against minio:
# run the minio container just like before docker run \ --publish 9000:9000 \ --name minio \ --detach \ --env 'MINIO_ACCESS_KEY=accesskey' \ --env 'MINIO_SECRET_KEY=secretkey' \ minio/minio \ server /data # Initialize the S3 (minio) repository passing # the AWS credentials as environment variables # and the repository pointing to our local minio # instance export AWS_ACCESS_KEY_ID=accesskey export AWS_SECRET_ACCESS_KEY=secretkey restic \ --repo s3:http://localhost:9000/restic-test \ init enter password for new backend: enter password again: created restic backend 0871674e7e at s3:http://localhost:9000/restic-test # now, perform the snapshot: restic \ --repo s3:http://localhost:9000/restic-test \ backup ./rootfs enter password for repository: password is correct scan [/tmp/cc/rootfs] scanned 1 directories, 2 files in 0:00 [0:00] 100.00% 0B/s 4B / 4B 3 / 3 items 0 errors ETA 0:00 duration: 0:00, 0.00MiB/s snapshot e870fc54 saved # check that restic created the contents there # in S3 (minio): aws \ --endpoint-url=http://localhost:9000 \ s3 ls --recursive \ s3://restic-test 2017-12-01 10:48:41 155 config 2017-12-01 10:49:01 178 data/25/25fe4bc29169f29... 2017-12-01 10:49:01 1312 data/5b/5b525003c0450a4... 2017-12-01 10:49:01 661 index/e691cc0bd3ebc2e9c... 2017-12-01 10:48:41 460 keys/50a8c58f10d1c4ae1d... 2017-12-01 10:49:01 247 snapshots/e870fc54bb482...
I was very happy knowing that there’s an easy way of creating these incremental snapshots using standard Linux utilities. For sure there are better ways (or more modern at least) but this one described in the article seems to be pretty much enough for various cases.
Restic reveals to be pretty good for my use case (performing some snapshots and having them sent directly to S3) but it has some limitations like not having compression. As I never used it very extensively, that’s the biggest one for me (ps.: if you want to see a comparison of Restic against some other tools, make sure you check https://github.com/gilbertchen/benchmarking).
Some resources used for this article are:
- GNU Tar Manual - Performing incremental dumps - great section in the
taron how to perform incremental backups;
- Source of the GNU-TAR homebrew formula;
- minio - the S3-compatible storage used in this article so simulate S3;
- github.com/restic/restic - the backup tool to use in comparison with the TAR method.
What about you? Have you been performing incremental backups? What are you using to do it? Please let me know, I’m cirowrc on Twitter and would really like to know more about!
Have a good one,