Last active
September 26, 2018 03:49
-
-
Save aganders3/6b40485d74d4b37a19cd0ca34f512cc8 to your computer and use it in GitHub Desktop.
My fabfile for deploying a Flask app on a Digital Ocean droplet
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from fabric import task | |
import os | |
import getpass | |
import subprocess | |
# you also need to install doctl and set it up for create/destroy to work | |
APP_NAME = 'the-name-of-your-app' | |
DOMAIN = 'example.com' # SSL is not in here yet | |
USER = 'your-username' | |
PUB_KEY = '~/.ssh/id_rsa.pub' | |
PUB_KEY_MD5 = 'your-public-key-md5' | |
NGINX_USER = 'www-data' | |
NGINX_USER_GROUP = 'www-data' | |
@task | |
def create(c): | |
create_droplet = ['doctl', 'compute', 'droplet', 'create'] | |
droplet_name = input("Enter a droplet name: ") | |
create_droplet += [droplet_name] | |
create_droplet += ['--size', 's-1vcpu-1gb'] | |
create_droplet += ['--image', 'ubuntu-18-04-x64'] | |
create_droplet += ['--region', 'nyc1'] | |
create_droplet += ['--ssh-keys', PUB_KEY_MD5] | |
create_droplet += ['--region', 'nyc1'] | |
create_droplet += ['--wait'] | |
create_droplet += ['--format', 'ID,Name,PublicIPv4'] | |
create_droplet += ['--no-header'] | |
p = subprocess.run(create_droplet, stdout=subprocess.PIPE) | |
droplet_info = p.stdout.decode('utf-8') | |
print("Created a new droplet!") | |
print("ID\tName\tIP") | |
print(droplet_info) | |
@task | |
def destroy(c): | |
prompt = "Enter the number of a droplet to destroy it: " | |
droplet_to_destroy, droplets = select_droplet(prompt) | |
if (droplet_to_destroy is None or | |
droplet_to_destroy < 0 or | |
droplet_to_destroy >= len(droplets)): | |
print("Invalid droplet selected") | |
return 1 | |
else: | |
drop_id, drop_name, drop_ip = droplets[droplet_to_destroy].split() | |
delete_droplet = ['doctl', 'compute', 'droplet', 'delete'] | |
delete_droplet += [str(drop_id)] | |
p = subprocess.run(delete_droplet) | |
def select_droplet(prompt="Select a droplet: "): | |
list_droplets = ['doctl', 'compute', 'droplet', 'list'] | |
list_droplets += ['--format', 'ID,Name,PublicIPv4'] | |
list_droplets += ['--no-header'] | |
p = subprocess.run(list_droplets, stdout=subprocess.PIPE) | |
droplets = p.stdout.decode('utf-8').split('\n')[:-1] | |
print("Index\t\tID\tName\tIP") | |
for i, droplet in enumerate(droplets): | |
print(i, ":\t", droplet) | |
try: | |
selected = int(input(prompt)) | |
except ValueError: | |
selected = None | |
finally: | |
return selected, droplets | |
@task | |
def init(c): | |
# install python3-pip python3-dev nginx | |
c.run('apt-get update') | |
c.run('apt-get install python3-pip python3-dev nginx -y') | |
# create the working directory for the app | |
c.run('pip3 install virtualenv') | |
c.run('mkdir -p /var/www/{}'.format(APP_NAME)) | |
# set up bare git repo separate from the work directory | |
c.run('mkdir -p /var/repo/{}.git'.format(APP_NAME)) | |
c.run('git init --bare /var/repo/{}.git'.format(APP_NAME)) | |
# set up post-receive hook for copying files to the work tree | |
with c.cd('/var/repo/{}.git/hooks'.format(APP_NAME)): | |
c.run('touch post-receive') | |
c.run('chmod +x post-receive') | |
# check out the files after a push | |
c.run('echo "#!/bin/sh" >> post-receive') | |
c.run(('echo "git --work-tree=/var/www/{0} ' | |
'--git-dir=/var/repo/{0}.git checkout -f" ' | |
'>> post-receive').format(APP_NAME)) | |
# create app virtual environment | |
c.run('virtualenv /var/www/{0}/{0}_env'.format(APP_NAME)) | |
# create a directory for the gunicorn socket(s) and database(s) | |
c.run('mkdir -p /run/gunicorn') | |
c.run('chown root:{} /run/gunicorn'.format(NGINX_USER_GROUP)) | |
c.run('chmod 770 /run/gunicorn') | |
c.run('chmod g+s /run/gunicorn') | |
# set up UFW to allow nginx and OpenSSH | |
c.run('ufw allow OpenSSH') | |
c.run('ufw allow "Nginx Full"') | |
c.run('ufw enable') | |
c.run('ufw status') | |
# TODO: set up domain records | |
# TODO: set up SSL with Let's Encrypt | |
# push up the code | |
update(c, first_push=True, | |
stop_server=False, | |
start_server=False) | |
# initialize the database | |
db_init(c) | |
@task | |
def adduser(c, username=USER, pubkey_file=PUB_KEY): | |
# create a new user and make it a sudoer | |
new_pass = getpass.getpass("Enter a password for the new user") | |
c.run('adduser --disabled-password --gecos "" {}'.format(username)) | |
c.run('usermod -aG sudo {}'.format(username)) | |
c.run('echo "{}:{}" | chpasswd'.format(username, new_pass)) | |
# add public key | |
with open(os.path.expanduser(pubkey_file)) as fd: | |
ssh_key = fd.readline().strip() | |
c.run('mkdir -p -m 700 /home/{}/.ssh'.format(username)) | |
c.run('chown {0}:{0} /home/{0}/.ssh'.format(username)) | |
c.run('touch /home/{}/.ssh/authorized_keys'.format(username)) | |
c.run('echo "{}" >> /home/{}/.ssh/authorized_keys'.format(ssh_key, username)) | |
c.run('chown {0}:{0} /home/{0}/.ssh/authorized_keys'.format(username)) | |
c.run('chmod 600 /home/{}/.ssh/authorized_keys'.format(username)) | |
@task | |
def update(c, first_push=False, stop_server=True, start_server=True): | |
if stop_server: | |
stop(c) | |
remote_name = input("Enter a name for the git remote destination [live]: ") or 'live' | |
if first_push: | |
# set remote for local git with server name | |
c.local('git remote add {} '.format(remote_name) + | |
'ssh://{}@{}/var/repo/{}.git'.format(c.user, | |
c.host, | |
APP_NAME)) | |
c.local('git push --set-upstream {} master'.format(remote_name)) | |
else: | |
c.local('git push {}'.format(remote_name)) | |
with c.cd('/var/www/{0}'.format(APP_NAME)): | |
# update venv | |
c.run('./{}_env/bin/pip install -r requirements.txt'.format(APP_NAME)) | |
# link <app-name>.nginx to sites-available | |
c.run('ln -f -s /var/www/{0}/{0}.nginx /etc/nginx/sites-available/{0}'.format(APP_NAME)) | |
# enable systemd service | |
c.run('systemctl -f enable /var/www/{0}/{0}.service'.format(APP_NAME)) | |
# add any cron job(s) here | |
c.run('crontab -u {0} /var/www/{1}/{1}.crontab'.format(NGINX_USER, APP_NAME)) | |
if start_server: | |
start(c) | |
@task | |
def start(c): | |
# start gunicorn | |
c.run('systemctl start {}'.format(APP_NAME)) | |
# remove default from sites-enabled | |
c.run('rm -f /etc/nginx/sites-enabled/default') | |
# link nginx conf file to sites-enabled | |
c.run('ln -s /etc/nginx/sites-available/{0} /etc/nginx/sites-enabled/{0}'.format(APP_NAME)) | |
# restart nginx | |
c.run('systemctl restart nginx') | |
@task | |
def stop(c): | |
# stop gunicorn | |
c.run('systemctl stop {}'.format(APP_NAME)) | |
# unlink nginx conf file to sites-enabled | |
c.run('rm -f /etc/nginx/sites-enabled/{}'.format(APP_NAME)) | |
# restart nginx | |
c.run('systemctl restart nginx') | |
@task | |
def db_init(c): | |
# check if DB already exists, request db_kill if you really want to | |
# overwrite it | |
with c.cd('/var/www/{}'.format(APP_NAME)), c.prefix('export DB_BASE_DIR="/run/gunicorn"'): | |
c.run('source {0}_env/bin/activate; echo -e "from {0} import db\ndb.create_all()" | python'.format(APP_NAME)) | |
@task | |
def db_kill(c): | |
print("YOU ARE ABOUT TO DELETE THIS DATABASE FILE ON THE SERVER:") | |
c.run('ls -l /run/gunicorn/{}.db'.format(APP_NAME)) | |
print("WARNING: THIS ACTION CANNOT BE UNDONE") | |
kill = input("ARE YOU SURE? [y/N]: ") | |
if kill == 'y': | |
c.run('rm -f /run/gunicorn/{}.db'.format(APP_NAME)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment