Yesterday I wrote briefly about my development to deployment pipeline, and I very briefly mentioned Fabric, which is a Capistrano-like tool written in Python that helps remove mindless repetition from deployment.
For a long time I was content with the SSH into the server and do stuff approach to deployment (I'm not managing any large deployments, and am usually dealing with small projects without the resources for staging servers and such), but I've been intoxicated by Fabric's sweet poison, and I think it's a noticable improvement--even for the smallest of deployments.
Briefly, I think using Fabric (or something similar) is a real win because:
You never have to leave your system. You get to use your tools, your files, your setup.
You only have to type one command to perform the deployment. Even for simple deployments you're still typing one or two dozen shell commands (changing directories, pulling from version control, rebooting the server), and you just don't have to type those commands anymore.
Even if that seems like a small gain, I think it's more than that, because you can actually deploy without breaking your programming flow.
With a good deployment solution, you type once and then run it many times, so you're more likely to do things the right way instead of the quick way. (Repetition leads to boredom, boredom to horrifying mistakes, horrifying mistakes to God-I-wish-I-was-still-bored, and it goes downhill from there.)
Other developers, even those who wake up with a cold sweat when dreaming about linux administration, can deploy and rollback as well. If you throw a five minute GUI on top, then even your grandmother can be out there deploying and rolling back your production servers.
By abstracting the deployment process into a simple deployment script you're making it easiser for other people to do your job for you. Which is a good thing. Unless you're worried that you seem unproductive at work. Then you might not like Fabric.
It scales well to large environments.
Now let's take a brief stroll through my Fabric setup.
Using Fabric
You can install Fabric using easy_install, but I'm using
the current Git checkout, and the project is still young enough
that code written for the latest packaged release will not work
for the current development head1. So I recommend you install it
from its Git repository.
git clone git://git.savannah.nongnu.org/fab.git
cd fab
sudo python setup.py build install
Now go into your Django project directory and create a
file named fabfile.py. Let's start out by adding a few
utility functions I use for managing Git repositories.
def git_pull():
"Updates the repository."
run("cd ~/git/$(repo)/; git pull $(parent) $(branch)")
def git_reset():
"Resets the repository to specified version."
run("cd ~/git/$(repo)/; git reset --hard $(hash)")
Note a few important things:
The connection between Fabric and your server is not a persistent connection. That means you cannot use commands like
cdin multiple commands and expect them to work.# this works as expected run("cd ~/git/$(repo)/; git pull $(parent) $(branch)") # this will run the command in the wrong directory run("cd ~/git/$(repo)/") run("git pull $(parent) $(branch)")
Fabric uses its own system for string interpolation. Usage is like this:
# standard python string interpolation run("git pull %s %s" % ("origin", "master")) # Fabric string interpolation config.parent = "origin" config.branch = "master" run("git pull $(parent) $(branch)")
Fabric's approach makes it easier to pass state around between various commands. Well, that's their story anyway. I'm not going to defend the approach beyond saying that I suspect they're working around some pain point in their architecture.
Now that we have those Git utilities, lets fill
in the rest of the fabfile.py so that it can
take advantage of the utilities.
The first thing you should do is create one or more functions, one for each set of servers you want to handle in a different way. For example, if you have a cluster of production servers and a cluster of staging servers, then you'd write something like this:
def production():
config.fab_hosts = ['a.example.com','b.example.com']
config.repos = (('project','origin','master'),
('app','origin','master'))
def staging():
config.fab_hosts = ['a.staging_example.com','b.staging_example.com']
config.repos = (('project','origin','master'),
('app','origin','master'))
The fab_hosts value is a special value that Fabric uses
to determine the servers to run commands on, but the repos
value is something that I added in myself for my scripts.
You can add as many arbitrary values to config as your heart desires.
Now to put together the three functions I use for managing deployment.
def reboot():
"Reboot Apache2 server."
sudo("apache2ctl graceful")
def pull():
require('fab_hosts', provided_by=[production])
for repo, parent, branch in config.repos:
config.repo = repo
config.parent = parent
config.branch = branch
invoke(git_pull)
def reset(repo, hash):
"""
Reset all git repositories to specified hash.
Usage:
fab reset:repo=my_repo,hash=etcetc123
"""
require("fab_hosts", provided_by=[staging, production])
config.hash = hash
config.repo = repo
invoke(git_reset)
To understand this code, it helps to know that there are a few magic functions that Fabric uses for deployment.
-
runruns a command on another machine. -
localruns a command on your local machine. -
sudoruns a command on another machine as root. -
putsends a file to another machine from your local machine. -
invokeruns another function with the current function's configuration context. -
requirecreates dependencies among various functions, to prevent functions from being run without their prerequisites.
Now deployment is as easy as:
cd directory_with_fabfile/
# reboot the server
fab production reboot
# you can do all this on staging too
fab staging reboot
# update the git repositories
fab production pull
# update repositories then reboot
fab production pull reboot
# reset repositories then reboot
fab production reset:repo=my_app_repo,hash=13klafeomaeio23 reboot
This makes administration painless and quick as hell. But, hey, lets add just a little more spice. Let's add a deployment option that will only deploy your project if all its tests pass:
def test():
local("python manage.py test", fail="abort")
And now you could deploy if and only if all tests pass by using this command:
fab test pull reboot
Now, let's say that you have some bad apples on your development team
and even with the test option they still keep pushing broken
code to your server. You could update pull to look like this:
def pull():
require('fab_hosts', provided_by=[production])
invoke(test)
for repo, parent, branch in config.repos:
config.repo = repo
config.parent = parent
config.branch = branch
invoke(git_pull)
Now they won't be able to deploy their changes unless all the
tests pass successfully. (Or they edit fabfile.py, but that
isn't the point. ;)
I hope you're starting to get some ideas about how integrating Fabric into your deployment workflow can make your life a bit happier. It's useful, and it's fun. What more can you ask for?
Full Source
Here is the complete code we put together in this article.
REPOS = (("my_project", "origin", "master"),
("my_app", "origin", "master"))
def production():
config.fab_hosts = ['a.example.com']
config.repos = REPOS
def staging():
config.fab_hosts = ['a.staging_example.com']
config.repos = REPOS
def git_pull():
"Updates the repository."
run("cd ~/git/$(repo)/; git pull $(parent) $(branch)")
def git_reset():
"Resets the repository to specified version."
run("cd ~/git/$(repo)/; git reset --hard $(hash)")
def reboot():
"Reboot Apache2 server."
sudo("apache2ctl graceful")
def pull():
require('fab_hosts', provided_by=[production])
for repo, parent, branch in config.repos:
config.repo = repo
config.parent = parent
config.branch = branch
invoke(git_pull)
def test():
local("python manage.py test", fail='abort')
def reset(repo, hash):
"""
Reset all git repositories to specified hash.
Usage:
fab reset:repo=my_repo,hash=etcetc123
"""
require("fab_hosts", provided_by=[production])
config.hash = hash
config.repo = repo
invoke(git_reset)
As always, comments, suggestions and complaints are welcome. If you're looking to do more complex things with Fabric, then I highly recommend browsing he mailing lists, which managed to answer all my questions without me actually asking any.
For example, you use
set(fab_hosts=['example.com'])in the latest package, and useconfig.fab_hosts=['example.com']in the latest version in the Git repository.↩
Hey, I am happily using capistrano to deploy my django projects right now, but thanks for this great introduction to Fabric anyways. It will probably come in handy at some time.
Really interesting. I had fabric in my scope but had not the opportunity so far to play with.
It encourages me to use it instead of thie perl script I must use and that nobody can maintain nor improve :-(
Did you face some limitations with fabric so far ?
I haven't really run into any limitations with Fabric, although it can be a bit confusing to get started since most tutorials are out of date (which is almost inevitable because the project is changing rapidly), and it relies heavily on magic. Much like Django eventually went through a magic removal stage, I think Fabric will eventually get less and less magical and more and more predictable as it ages.
Ok, thanks for your feedback.
Hey Will -- looks like you got picked up on Reddit, good job =)
I'd definitely love to get some of the magic out of Fabric once the feature set has settled down some.
As I was explaining to someone on the mailing list today, the main reason I can think of for the existing magic is to alter the namespace within Fabric commands (the user-defined functions) to allow access to the operations such as run(), sudo() and so forth, without needing to explicitly import them.
In Capistrano, which is Ruby, that sort of thing isn't even an issue because Ruby and Rails play so fast and loose with the namespacing and importing side of things, which is why it's well suited to DSLs. Python and the 'explicit over implicit' creed are naturally at odds with such an approach.
I can't say for now how exactly Fabric might try to become less magical, but again, I'm quite interested in trying to keep things Pythonic whenever possible :) so we'll see!
Do you also know about django-fabric? It's a custom command management plugin for django, so you can do 'manage.py fab'
It can be found at https://launchpad.net/django-fabric
Paul,
Although I'm all for the idea of integration, I'm not really sure what the benefit of using django-fabric is? Instead of typing
fab deployI would need to typepython manage.py fab deploy, along with needing to add thefabapp to myINSTALLED_APPLICATIONSsetting for my project.Do you have any scenarios where django-fabric is a clear improvement over using Fabric normally?
Will,
I agree with you that django-fabric doesn't make any sense.
However, I'm liking the thought of having all manage.py commands as part of my fabfile.py so I can do everything with fabric, e.g.: "fab syncdb" etc. just like you did with your test target.
I'll see if I can figure out a way to have any unknown commands passed through to fabric to make the fabfile easier.
The design of fabric is broken from the start if you have to interpolate strings inside of commands that are going to be run via the shell.
As soon as "dir" has any shell meta-characters in it you are dead in the water.
I don't know why commands aren't built up and executed as lists the way they should be. execve() takes a list and the python language supports lists - it seems like a no-brainer to design an API that runs commands around lists to avoid this problem all together. But time and time again you get an API built around strings.
These are problems waiting to happen.
I migrated an existing deployment script to Fabric recently. Aside from the annoying namespace system the experience was pretty positive. One thing I couldn't figure out though was how to get the output from run() back into a python variable.. I might look into implementing this feature if it isn't part of the project at the moment.
You can simply write:
with the trunk version of Fabric, I am using it extensively and this is working a wonder.
I have found, that invoke command, repeated in the loop, actually runs only once. All following tries to invoke same function fail with warning "Skipping xxxxxx (already invoked).".
I looked into the fabric's sources, and found, this interesting docstring in the infoke function:
So, to make it works for different repositories, you need to use such code:
@@ python def git_pull(repo, parent, branch): "Updates the repository." config.repo = repo config.parent = parent config.branch = branch run("cd ~/git/$(repo)/; git pull $(parent) $(branch)")
def pull(): require('fab_hosts', provided_by=[production]) for repo, parent, branch in config.repos: invoke((git_pull, dict(repo=repo, parent=parent, branch=branch))) @@
Hey, I just stumbled on your page from delicious. I also use Fabric to deploy my Django applications and it's made things much easier. I wrote a post a few days ago about how I do it: Deploying a Site with Fabric and Mercurial.
Your post gave me some new ideas for my fabfiles like server rebooting and deploying a specific version. Thanks!
run() and sudo() returns the output of the command so you can do:
foo = run("ls -l")
But this does not work with local(), why is that?
I'm running into the same issue where I need to determine which war file to deploy so I'm trying to run:
warfile = local('ls -l *.war | head -1')
then I want to use the variable $(warfile) throughout my deploy script.
But currently, I can't get "local" to do that...
I really enjoy your posts!
It would be great if you could post an updated version of your fabfile for Fabric 0.9.
Reply to this entry