I first heard about Fabric a couple years ago while at Lolapps and liked the idea of:

writing deployment and sysadmin scripts in a language other than Bash

that language being Python, which we used everywhere else

but we already had a huge swath of shell scripts that worked well (and truth be told, Bash isn’t really that bad). But now that we have at clean slate for Rollbar, Fabric it is.

I wanted a simple deployment script that would do the following:

check to make sure it’s running as the user “deploy” (since that’s the user that has ssh keys set up and owns the code on the remote machines) for each webserver: git pull pip install -r requirements.txt in series, restart each web process make an HTTP POST to our deploys api to record that the deploy completed successfully

Here’s my first attempt:

import sys from fabric . api import run , local , cd , env , roles , execute import requests env . hosts = [ 'web1' , 'web2' ] def deploy ( ) : check_user ( ) update_and_restart ( ) rollbar_record_deploy ( ) def update_and_restart ( ) : code_dir = '/home/deploy/www/mox' with cd ( code_dir ) : run ( "git pull" ) run ( "pip install -r requirements.txt" ) run ( "supervisorctl restart web1" ) run ( "supervisorctl restart web2" ) def check_user ( ) : if local ( 'whoami' , capture = True ) != 'deploy' : print "This command should be run as deploy. Run like: sudo -u deploy fab deploy" sys . exit ( 1 ) def rollbar_record_deploy ( ) : access_token = local ( "grep 'rollbar.access_token' production.ini | sed 's/^.* = //g'" , capture = True ) environment = 'production' local_username = local ( 'whoami' , capture = True ) revision = local ( 'git log -n 1 --pretty=format:"%H"' , capture = True ) resp = requests . post ( 'https://api.rollbar.com/api/1/deploy/' , { 'access_token' : access_token , 'environment' : environment , 'local_username' : local_username , 'revision' : revision } , timeout = 3 ) if resp . status_code == 200 : print "Deploy recorded successfully" else : print "Error recording deploy:" , resp . text

Looks close-ish, right? It knows which hosts to deploy to, checks that it’s running as deploy, updates and restarts each host, and records the deploy. Here’s the output:

$ sudo -u deploy fab deploy ( env-mox ) [ brian@dev mox ] $ sudo -u deploy fab deploy [ sudo ] password for brian: [ web1 ] Executing task 'deploy' [ localhost ] local: whoami [ web1 ] run: git pull [ web1 ] out: remote: Counting objects: 8 , done. [ web1 ] out: remote: Compressing objects: 100 % ( 4 /4 ) , done. [ web1 ] out: remote: Total 6 ( delta 4 ) , reused 4 ( delta 2 ) [ web1 ] out: Unpacking objects: 100 % ( 6 /6 ) , done. [ web1 ] out: From github.com:brianr/mox [ web1 ] out: c731b57 .. 1d365e0 master - > origin/master [ web1 ] out: Updating c731b57 .. 1d365e0 [ web1 ] out: Fast-forward [ web1 ] out: fabfile.py | 8 ++++---- [ web1 ] out: 1 file changed, 4 insertions ( + ) , 4 deletions ( - ) [ web1 ] run: pip install -r requirements.txt [ web1 ] out: Requirement already satisfied ( use --upgrade to upgrade ) : Beaker == 1.6 .3 in /home/deploy/env-mox/lib/python2.7/site-packages ( from -r requirements.txt ( line 1 )) [ web1 ] out: Cleaning up .. . [ web1 ] run: supervisorctl restart web1 [ web1 ] out: web1: stopped [ web1 ] out: web1: started [ web1 ] run: supervisorctl restart web2 [ web1 ] out: web2: stopped [ web1 ] out: web2: started [ localhost ] local: grep 'rollbar.access_token' production.ini | sed 's/^.* = //g' [ localhost ] local: whoami [ localhost ] local: git log -n 1 --pretty = format: "%H" Deploy recorded successfully. Deploy id: 307 [ web2 ] Executing task 'deploy' [ localhost ] local: whoami [ web2 ] run: git pull [ web2 ] out: remote: Counting objects: 8 , done. [ web2 ] out: remote: Compressing objects: 100 % ( 4 /4 ) , done. [ web2 ] out: remote: Total 6 ( delta 4 ) , reused 4 ( delta 2 ) [ web2 ] out: Unpacking objects: 100 % ( 6 /6 ) , done. [ web2 ] out: From github.com:brianr/mox [ web2 ] out: c731b57 .. 1d365e0 master - > origin/master [ web2 ] out: Updating c731b57 .. 1d365e0 [ web2 ] out: Fast-forward [ web2 ] out: fabfile.py | 8 ++++---- [ web2 ] out: 1 file changed, 4 insertions ( + ) , 4 deletions ( - ) [ web2 ] run: pip install -r requirements.txt [ web2 ] out: Requirement already satisfied ( use --upgrade to upgrade ) : Beaker == 1.6 .3 in /home/deploy/env-mox/lib/python2.7/site-packages ( from -r requirements.txt ( line 1 )) [ web2 ] out: Cleaning up .. . [ web2 ] run: supervisorctl restart web1 [ web2 ] out: web1: stopped [ web2 ] out: web1: started [ web2 ] run: supervisorctl restart web2 [ web2 ] out: web2: stopped [ web2 ] out: web2: started [ localhost ] local: grep 'rollbar.access_token' production.ini | sed 's/^.* = //g' [ localhost ] local: whoami [ localhost ] local: git log -n 1 --pretty = format: "%H" Deploy recorded successfully. Deploy id: 308 Done. Disconnecting from web2 .. . done. Disconnecting from web1 .. . done.

Lots of good things happening. But it’s doing the whole process - check_user , update_and_restart , rollbar_record_deploy - twice, once for each host. The duplicate check_user just slows things down, but the duplicate rollbar_record_deploy is going to mess with our deploy history, and it’s only going to get worse as we add more servers.

Fabric’s solution to this, described in their docs, is “roles”. We can map hosts to roles, then decorate tasks with which roles they apply to. Here we replace the env.hosts declaration with env.roledefs , decorate update_and_restart with @roles , and call update_and_restart with execute so that the @roles decorator is honored:

import sys from fabric . api import run , local , cd , env , roles , execute import requests env . roledefs = { 'web' : [ 'web1' , 'web2' ] } def deploy ( ) : check_user ( ) execute ( update_and_restart ) rollbar_record_deploy ( ) @roles ( 'web' ) def update_and_restart ( ) : code_dir = '/home/deploy/www/mox' with cd ( code_dir ) : run ( "git pull" ) run ( "pip install -r requirements.txt" ) run ( "supervisorctl restart web1" ) run ( "supervisorctl restart web2" ) def check_user ( ) : if local ( 'whoami' , capture = True ) != 'deploy' : print "This command should be run as deploy. Run like: sudo -u deploy fab deploy" sys . exit ( 1 ) def rollbar_record_deploy ( ) : access_token = local ( "grep 'rollbar.access_token' production.ini | sed 's/^.* = //g'" , capture = True ) environment = 'production' local_username = local ( 'whoami' , capture = True ) revision = local ( 'git log -n 1 --pretty=format:"%H"' , capture = True ) resp = requests . post ( 'https://api.rollbar.com/api/1/deploy/' , { 'access_token' : access_token , 'environment' : environment , 'local_username' : local_username , 'revision' : revision } , timeout = 3 ) if resp . status_code == 200 : print "Deploy recorded successfully" else : print "Error recording deploy:" , resp . text

Here’s the output:

( env-mox ) [ brian@dev mox ] $ sudo -u deploy fab deploy [ sudo ] password for brian: [ localhost ] local: whoami [ web1 ] Executing task 'update_and_restart' [ web1 ] run: git pull [ web1 ] out: Already up-to-date. [ web1 ] run: pip install -r requirements.txt [ web1 ] out: Requirement already satisfied ( use --upgrade to upgrade ) : Beaker == 1.6 .3 in /home/deploy/env-mox/lib/python2.7/site-packages ( from -r requirements.txt ( line 1 )) [ web1 ] out: Cleaning up .. . [ web1 ] run: supervisorctl restart web1 [ web1 ] out: web1: stopped [ web1 ] out: web1: started [ web1 ] run: supervisorctl restart web2 [ web1 ] out: web2: stopped [ web1 ] out: web2: started [ web2 ] Executing task 'update_and_restart' [ web2 ] run: git pull [ web2 ] out: Already up-to-date. [ web2 ] run: pip install -r requirements.txt [ web2 ] out: Requirement already satisfied ( use --upgrade to upgrade ) : Beaker == 1.6 .3 in /home/deploy/env-mox/lib/python2.7/site-packages ( from -r requirements.txt ( line 1 )) [ web2 ] out: Cleaning up .. . [ web2 ] run: supervisorctl restart web1 [ web2 ] out: web1: stopped [ web2 ] out: web1: started [ web2 ] run: supervisorctl restart web2 [ web2 ] out: web2: stopped [ web2 ] out: web2: started [ localhost ] local: grep 'rollbar.access_token' production.ini | sed 's/^.* = //g' [ localhost ] local: whoami [ localhost ] local: git log -n 1 --pretty = format: "%H" Deploy recorded successfully. Deploy id: 309 Done. Disconnecting from web2 .. . done. Disconnecting from web1 .. . done.