Automated deploys with Fabric

The first time you deploy a new application, you probably do it manually in the terminal. This is great for learning and prototyping, but soon you will want to automate this process. The faster your deployment process is, the more likely you are to iterate quickly and make progress.

Unfortunately, there is a huge gap in complexity between manual terminal commands and automation tools like Ansible. Instead, consider scripting your deploy process with Fabric, a simple yet powerful Python library for automating SSH sessions. You can level up to Ansible if you ever actually need it.


Why Fabric

You can do nearly anything you can imagine using Bash scripts, but Bash isn't very comfortable to work in. Fabric powers up your Python scripts by making it easy to do common tasks on a remote machine:

Because you have the full power of Python as well, it's easy to handle errors, perform actions conditionally, and pull in data from your filesystem or the Internet.

Deploying a Django app with Fabric

Here I'll show a quick example of a Fabric-powered script that deploys a Django application.

This script is wrapped in a Django management command, so it is runs as ./manage.py deploy and has access to the application settings, etc.

django_app/management/commands/deploy.py
import getpass

from django.core.management.base import BaseCommand

from fabric import Connection, Config


class Command(BaseCommand):
    help = "Deploy the application."

    # This is a path on the remote server, not your local machine
    MANAGE_PY = "/path/to/virtualenv/bin/python manage.py"

    def add_arguments(self, parser):
        pass

    def handle(self, *args, **options):
        # Get the sudo password interactively.
        # This can be automated if you have a safe way to store the password.
        sudo_pass = getpass.getpass("Enter sudo password for the remote user:\n")
        config = Config(overrides={"sudo": {"password": sudo_pass}})

        # Open an SSH session
        with Connection("yourhost.com", config=config) as c:

            # Set the working directory
            with c.cd("path/to/project"):

                # Fetch latest code using a read-only git account
                c.run("git pull")

                # These are Django-specific commands to verify config, 
                # run any database migrations, and prepare static files
                c.run(f"{self.MANAGE_PY} check --deploy --fail-level=WARNING")
                c.run(f"{self.MANAGE_PY} migrate")
                c.run(f"{self.MANAGE_PY} collectstatic --no-input")

            # Since systemd manages my web server, I use it to roll out
            # workers running the new application version
            c.sudo("systemctl reload myproject.service")

        print("\nOK!\n")

The convenience of a scripted deploy versus manual SSH makes a huge difference in developer ergonomics and reduces the chance for errors. If any command in the script fails, Fabric raises an exception, halts the script, and dumps a stack trace.