angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

PostgreSQL back-up met Docker SDK voor Python

Vermijd complexe Bash-scripts. Schrijf uw scripts in Python!

9 maart 2023
post main image
https://www.pexels.com/nl-nl/@pixabay/

Dit is een kort bericht over de back-up van een Dockerized PostgreSQL database. Om toegang te krijgen tot de database draaien we meestal een Bash-script op de host, met commando's als:

docker exec -t <container> bash -c '<command>'

In deze post vervangen we ons Bash-script door een Python -script. Waarom? Omdat we Python kennen en programmeren in Bash tijdrovend kan zijn. Hoewel we hier 'subprocess' kunnen gebruiken om alle systeemcommando's uit te voeren, nemen we de Docker SDK voor Python op en krijgen we veel extra's. Zoals altijd draai ik dit op Ubuntu (22.04).

Back-up operatie

We zullen in de stappen een back-up uitvoeren:

  • Back-up database naar een tijdelijk bestand binnen de container.
  • Kopieer het back-up bestand buiten de container.
  • Verwijder het tijdelijke bestand.
  • Toon de tijd van de back-up operatie.

Het Bash back-up script

Hieronder staat het Bash back-up script. Het controleert op fouten tijdens de back-upoperatie en de kopieeroperatie. Het script begint met onze container en database parameters.

#!/usr/bin/bash
# backup.sh
pg_container=8c49633bda68
pg_user=postgres
db_name=my_db
db_user=postgres

# start
SECONDS=0

# backupfile inside docker container
tmp_backupfile=/tmp/${db_name}.pg_dump
# backupfile on the host
backupfile=${db_name}.pg_dump.`date +%Y%m%d"_"%H%M%S`.sql

echo "tmp_backupfile = ${tmp_backupfile}"
echo "backupfile = ${backupfile}"

echo "removing previous tmp_backupfile ..."
docker exec -t ${pg_container} bash -c 'rm -f -- ${0}' ${tmp_backupfile}

echo "dumping database to tmp_backupfile ..."
docker exec -t --user ${pg_user} ${pg_container} bash -c 'pg_dump ${0} -Fc -U ${1} -f ${2}' ${db_name} ${pg_user} ${tmp_backupfile}
exit_code=$?
if [ $exit_code -ne 0 ] ; then
    echo "dump error, exit_code = ${exit_code}"
    exit $exit_code
fi

echo "copying tmp_backupfile to backupfile ..."
docker cp "${pg_container}":${tmp_backupfile} ${backupfile}
exit_code=$?
if [ $exit_code -ne 0 ] ; then
    echo "copy error, exit_code = ${exit_code}"
    exit $exit_code
fi

echo "removing tmp_backupfile ..."
docker exec -t ${pg_container} bash -c 'rm -f -- ${0}' ${tmp_backupfile}

fsize=`stat --printf="%s" ${backupfile}`
elapsed_secs=$SECONDS

echo "ready, backupfile = ${backupfile}, size = ${fsize}, seconds = ${elapsed_secs}"

Het Python back-up script

Voor het Python script installeren we eerst de Python SDK voor Docker (in een virtual environment):

pip install docker

Hieronder staat het Python back-up script. Het controleert op fouten tijdens de back-upoperatie en de kopieeroperatie.

# backup.py
import datetime
import logging
import os
import subprocess
import sys
import time

import docker

def get_logger():
    logger_format = '[%(asctime)s] [%(levelname)s] %(message)s'
    logger = logging.getLogger(__name__)
    logger.setLevel(logging.DEBUG)
    # console
    console_handler = logging.StreamHandler(sys.stdout)
    console_handler.setLevel(logging.DEBUG)
    console_handler.setFormatter(logging.Formatter(logger_format))
    logger.addHandler(console_handler)                                            
    return logger

logger = get_logger()

class DockerUtils:
    def __init__(
        self,
        container=None,
    ):
        self.client = docker.from_env()
        self.container = self.client.containers.get(container)

    def docker_exec(self, cmd, **kwargs):
        return self.container.exec_run(cmd, **kwargs)

    def remove_container_file(self, f):
        r = self.docker_exec('ls ' + f)
        if r.exit_code == 0:
            r = self.docker_exec('rm ' + f)

    def docker_cp_from_container(self, src, dst):
        r = subprocess.run(['docker', 'cp', self.container.short_id + ':' + src, dst])
        return r

def main():
    pg_container = '8c49633bda68'
    pg_user = 'postgres'
    db_name = 'my_db'
    db_user = 'postgres'

    # start
    dt_start = datetime.datetime.now()

    # backupfile inside docker container
    tmp_backupfile = os.path.join('/tmp/', db_name + '.pg_dump')
    # backupfile on the host
    backupfile = './' + db_name + '.pg_dump.' + datetime.datetime.now().strftime('%Y%m%d_%H%M%S')

    logger.debug('tmp_backupfile = {}'.format(tmp_backupfile))
    logger.debug('backupfile = {}'.format(backupfile))

    # instantiate and set container
    du = DockerUtils(container=pg_container)

    logger.debug('removing previous tmp_backupfile ...')
    du.remove_container_file(tmp_backupfile)

    logger.debug('dumping database to tmp_backupfile ...')
    cmd = 'pg_dump {0} -Fc -U {1} -f {2}'.format(db_name, db_user, tmp_backupfile)
    kwargs = {'user': pg_user}
    r = du.docker_exec(cmd, **kwargs)
    if r.exit_code != 0:
        logger.error('dump error: {}'.format(str(r.output.decode('utf-8'))))
        sys.exit()
    
    logger.debug('copying tmp_backupfile to backupfile ...')
    r = du.docker_cp_from_container(tmp_backupfile, backupfile)
    if r.returncode != 0:
        logger.error('copy error = {}'.format(r.returncode))
        sys.exit()

    logger.debug('removing tmp_backupfile ...')
    du.remove_container_file(tmp_backupfile)

    fsize = os.stat(backupfile).st_size
    elapsed_secs = int((datetime.datetime.now() - dt_start).total_seconds())

    logger.info('ready, backupfile = {}, size = {}, seconds = {}'.format(backupfile, fsize, elapsed_secs))

if __name__ == "__main__":
    main()

Draaien zonder virtual environment

Maar wacht, met Python hebben we een virtual environment nodig. Om dit te voorkomen, kunnen we een executable maken met PyInstaller. Installeer eerst PyInstaller:

pip install pyinstaller

Maak vervolgens de executable met het volgende commando:

pyinstaller --onefile backup.py

Hiermee wordt een executable gemaakt in de directory './dist':

dist/backup

We kunnen dit uitvoeren en deze executable in ons pad zetten.

Samenvatting

We hadden de Docker SDK voor Python hier niet echt nodig, we hadden ook 'subprocess' kunnen gebruiken, maar door het op te nemen kunnen we ook veel andere dingen doen.

Er is niet veel verschil tussen het Bash script en het Python script. Een paar regels schrijven in Bash is gemakkelijk. Maar zodra we wat meer functionaliteit en controle willen, kan het erg tijdrovend worden. Tenzij je een Bash-goeroe bent, moet je commando's opzoeken, bestuderen hoe je functies schrijft, dingen testen. Bij het schrijven van een Python script hebben we nog steeds kennis nodig van Linux commando's, maar we kunnen dit beperken tot de paar functies die we nodig hebben. Voor het overige kunnen we onze Python kennis gebruiken.

Links / credits

Docker - docker cp
https://docs.docker.com/engine/reference/commandline/cp

Docker SDK for Python
https://docker-py.readthedocs.io

Dockerize & Backup A Postgres Database
https://dev.to/mattcale/dockerize-backup-a-postgres-database-2b1l

PostgreSQL - pg_dump
https://www.postgresql.org/docs/current/app-pgdump.html

PyInstaller
https://pyinstaller.org

Why is pg_restore segfaulting in Docker?
https://stackoverflow.com/questions/63934856/why-is-pg-restore-segfaulting-in-docker

Lees meer

Docker

Laat een reactie achter

Reageer anoniem of log in om commentaar te geven.

Opmerkingen

Laat een antwoord achter

Antwoord anoniem of log in om te antwoorden.