Cogito ergo sum

How to use Fabric to deploy a flask web application

Lately, I have been busy making a deploy script for a Flask API. I have never worked before with Flask and I just got to know Flask a couple of months ago. I have to admit, there are not a lot of resources online explaining how to work exactly with Fabric. Although, the website of fabric (fabfile) has good and detailed documentation, some idea’s aren’t documented or explained quite well.

In this post I will show you how to make a deploy script using Fabric to deploy a simple Flask app to a web server online. The fabric script will create new directories, clone a github repository for the first time if it’s not done before otherwise it will pull from the repository, activate the python virtual environment, install all required packages in the requirements.txt file, create a symlink and finally restart nginx and uwsgi if they are installed and enabled on the server (CentOs).
In other words, this script covers all possible operations that could be run on a server.

Used software

  1. Python 3.6.x
  2. fabric 2.4.0
  3. Ubuntu 18

The deployment kit consists of two files
1) The actual fabfile.py which contains the deploy script:

from pathlib import Path
 
import logging as logger
import os
import time

from click import Path
from dotenv import load_dotenv
from fabric import Connection as connection, task


@task
def deploy(ctx, env=None, branch=None):
    logger.basicConfig(level=logger.INFO)
    logger.basicConfig(format='%(name)s --------- %(message)s')

    if env is None or branch is None:
        logger.error("Env variable and branch name are required!, try to call it as follows : ")
        logger.error("fab deploy -e YOUR_ENV_FILE.env -b BRANCH_NAME")
        exit()
    time_stamp = time.strftime('%Y%m%d_%H%M%S')
    # Load the env files
    if os.path.exists(env):
        load_dotenv(dotenv_path=env, verbose=True)
        logger.info("The ENV file is successfully loaded")
    else:
        logger.error("The ENV is not found")
        exit()

    user = os.getenv("DS_USER")
    host = os.getenv("DS_HOST")
    deploy_dir = os.getenv("DS_DEPLOY_DIR")
    cache_dir = os.getenv("DS_CACHE_DIR")
    repo_url = os.getenv("DS_REPO_URL")
    main_release_dir = os.path.join(deploy_dir, os.getenv("DS_RELEASE_DIR"))

    release_dir = os.path.join(main_release_dir, 'release-' + time_stamp)
    current_ver_dir = os.path.join(deploy_dir, os.getenv('DS_CURRENT_DIR'))
    with connection(host=host, user=user) as c:
        # This is an example of how to add an env variable to the deploy server
        c.run('echo export %s >> ~/.bashrc ' % 'SAMPLE_ENV_VAR=VALUE')
        c.run('source ~/.bashrc')
        c.run('echo $SAMPLE_ENV_VAR')  # to verify if it's set or not!

        if not os.path.isdir(Path(deploy_dir)):
            c.run('mkdir -p ' + deploy_dir)

        if not os.path.isdir(Path(main_release_dir)):
            logger.info("Creating Main release dir /releases/")
            c.run('mkdir -p ' + main_release_dir)
            c.run('mkdir -p ' + release_dir)

            if not os.path.isdir(Path(cache_dir)):
                logger.info("Creating Git cache dir")
                logger.info("Cloning Flask_api")
                c.run('git clone -b %s %s %s' % (branch, repo_url, cache_dir), warn=True)

        with c.cd(cache_dir):
            logger.info("Pulling from Flask_api repo")
            c.run('git pull')
            logger.info("Checkout %s from Flask_api repo" % branch)
            c.run('git checkout %s' % branch)
            c.run('git pull')

        with c.cd(cache_dir):
            c.run('rsync -a  --exclude .git . %s' % release_dir)

        logger.info("Change recursively the owner-group of release directory")
        c.run('chgrp -R %s %s' % (user, release_dir))

        with c.cd(release_dir):
            c.run('virtualenv flask_api_virtualenv')
            with c.prefix('source flask_api_virtualenv/bin/activate'):
                c.run('pip3.6 install -r requirements.txt')
                c.run('deactivate')

        logger.info("Creating the Symlink")
        c.run('ln -sTf %s %s' % (release_dir, current_ver_dir))

        logger.info("Restarting nginx")
        c.run('sudo /bin/systemctl restart nginx')

        logger.info("Restarting uWSGi")
        c.run('sudo /bin/systemctl restart uwsgi')

2) .env file which contain information about the deploy server and some other variables (try to keep the code clean from hard-coded values)

# The user will be used on the remote server to deploy Flask
DS_USER=svc.delpoy_peshmerge
# The hostname where Flask will be deployed
DS_HOST=localhost
# The directory where we want to deploy flask_api
DS_REPO_URL = git@github.com:peshmerge/flask_api.git
DS_DEPLOY_DIR=/var/www/flask_api
DS_RELEASE_DIR=releases
DS_CURRENT_DIR = current
DS_CACHE_DIR = cache

The followed policy for does the following:

    1. Creates releases: which contains sub-folders, each subfolder would be names as follows: release-{timestamp in this format %Y%m%d_%H%M%S}/
    2. Creates cache: which contains a clone of the repository, this folder is created once/
    3. Creates current: which is a symlink to the latest release folder (release-timestamp)
      Folder_structure
      The folder structure of the deployed flask app
    4. Creates a flask_api_virtualenv virtual environment, activate it and install all required packages.
    5. Restarting nginx and uwsgi (I assumed they are installed and the deploy user is allowed to execute these commands without using sudo and the uwsgi is correctly configured). I will write in a different post how you can install uwsgi and use it correctly with nginx to serve a flask web application.

I hope you enjoyed the blog post. Please feel free to ask me anything if it’s not clear!

About the author

Peshmerge Morad

A IT-student and a programmer based in the Netherlands whose interests span multiple fields.

Add comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

By Peshmerge Morad
Cogito ergo sum