Deploying
With
Fabric
Handling multiple servers and environments, and handling common deployment tasks.
By Brandon Konkle (@bkonkle)
For the Dallas Django User's Group
Fabric
http://fabfile.org
- A tool written in Python to help you run tasks on your systems
- Helps you build automated tasks on top of SSH connections
- Gives you tools for common tasks and multiple servers and roles
This Talk
- Will cover a way to manage multiple environments and roles.
- Will introduce a few real-world deployment and convenience tasks.
Managing
Multiple
Environments
The Goal
To select your environment as easily as this:
$ fab dev deploy
And to target specific server roles like this:
$ fab -R app dev mytask
Defining your environments
First, define the details of your environments in the fabfile:
ENVIRONMENTS = {
'prod': {
'roledefs': {
'web': ['webserver1.myapp.com'],
'app': ['appserver1.myapp.com', 'appserver2.myapp.com'],
'util': ['utilserver1.myapp.com'],
'db': ['dbserver1.myapp.com'],
},
},
'dev': {
'roledefs': {
'web': ['dev.myapp.com'],
'app': ['dev.myapp.com'],
'util': ['dev.myapp.com'],
'db': ['dev.myapp.com'],
},
'proj_rev': 'develop',
}
}
Set some defaults
To handle things that are the same in most of your environments, define some defaults:
DEFAULTS = {
'user': 'myappuser',
'root': '/opt/webapps/myapp',
'proj_rev': 'master',
}
As you can see, the dev environment above overrides the default "proj_rev" to deploy the "develop" branch instead of "master".
selecting an environment
To put these definitions to work, create a task to select the desired
environment:
def environment(name):
"""Environment selector"""
env.update(dict(DEFAULTS, **ENVIRONMENTS[name]))
env.name = name
# Set dependent attributes
env.proj_root = env.root + '/src/myapp'
env.pip_file = env.proj_root + '/requirements.pip'
This updates the "env" object with details of the environment, and sets a couple of dynamic attributes.
Minor Details
Fabric won't allow you to use the '-R' option to select a role unless you have roledefs before any tasks are run. To
work around that, add some placeholders:
env.roledefs = {'web': [], 'app': [], 'db': [], 'util': []}
Next, default to all roles if no specific roles or hosts were specific on the
command line:
if not env.get('roles') and not env.get('hosts'):
env.roles = env.roledefs.keys()
Aliases
At this point, you can begin selecting your environment like this:
$ fab environment:dev deploy
This is too verbose for me, however. I like to set up quick aliases:
def prod():
"""Shortcut for environment:prod"""
return environment("prod")
def dev():
"""Shortcut for environment:dev"""
return environment("dev")
Now you can just use "dev" and "prod" to select your environment.
Specific roles
To target specific roles, use
fabric.decorators.roles
and decorate your tasks with the roles they should run on.
@roles('app', 'util')
def deploy(rev=None):
# Your deploy code here
Requiring an Environment
Some tasks absolutely need to have an environment
selected before they can run.
from functools import wraps
from fabric.api import require
def requires_env(task_func):
"""Decorator for tasks that require an environment"""
@wraps(task_func)
def wrapper(*args, **kwargs):
require('name', provided_by=['prod', 'dev'])
return task_func(*args, **kwargs)
return wrapper
Now you can decorate your tasks like this:
@roles('app', 'util')
@requires_env
def deploy(rev=None):
# Your deploy code here
Handling
Deploy
Tasks
Getting started
You may want to add a quick helper function to run a command using your virtualenv environment:
@roles('app', 'util')
@requires_env
def ve_run(cmd):
return run('source {}/bin/activate; {}'.format(env.root, cmd))
Update your code
Start out with a task to update the code on your servers:
@roles('app', 'util')
@requires_env
def checkout(branch=None):
"""Updates project source"""
if branch is None:
branch = env.proj_rev
with cd(env.proj_root):
# Clean .pyc files
run('find . -name *.pyc -delete')
run('git fetch --all')
run('git checkout origin/{}'.format(branch))
This code also takes the extra step of cleaning *.pyc files, which you may or
may not need.
Refresh your requirements
Next, create a task to refresh your pip requirements:
@roles('app', 'util')
@requires_env
def requirements():
"""Update pip requirements"""
ve_run('pip install --exists-action=w -r {}'.format(env.pip_file))
# Check for environment-specific requirements
env_reqs = '{}/{}_requirements.pip'.format(env.proj_root, env.name)
if exists(env_reqs):
ve_run('pip install --exists-action=w -r {}'.format(env_reqs))
This also checks for environment specific requirements with names like "dev_requirements.pip".
Run migrations
Next you'll want to run any necessary migrations:
@roles('app', 'util')
@requires_env
def migrate():
"""Run south migrations"""
ve_run('manage.py migrate --noinput --no-initial-data')
collect Static files
You'll want to collect your staticfiles and take any other necessary actions for your static media, such as compiling Compass:
@roles('app')
@requires_env
def collectstatic():
with cd(env.proj_root):
ve_run("compass compile")
ve_run("manage.py collectstatic --noinput")
Restart Processes
Finally, you'll need to restart your processes:
@roles('app')
@requires_env
def restart():
"""Restart using upstart"""
sudo('restart myapp')
Deploy!
Put it all together, and what have you got?
@roles('app', 'util')
@requires_env
def deploy(branch=None):
"""
Run all the tasks for a normal deploy.
Branches and individual commits can be deployed with::
$ fab envname deploy:refname
"""
checkout(branch)
requirements()
migrate()
collectstatic()
restart()
Pick and choose
If you want to run tasks independently, specify them by name:
$ fab dev requirements restart
Convenience
Tasks
What version?
If you want to see what revision is currently on your servers, you can write a task
like this:
@roles('app', 'util')
@requires_env
def version():
"""Show last commit to repo on server"""
with cd(env.proj_root):
run('git log -1')
cut your losses
In times of great peril, you may need to restart your database server:
from fabric.contrib.console import confirm
@roles('db')
@requires_env
def restart_db():
if confirm("Are you sure you want to restart the database process?"):
sudo('/etc/init.d/postgresql restart')
Check the logs
Maybe you want a quick look at a log file?
@requires_env
@roles('app', 'util')
def django_log(lines=30):
"""Tail the Django Log"""
log_file = "{env.root}/var/log/django.log".format(env=env)
run("tail -n{lines} {file}".format(lines=lines, file=log_file))
Just the tip of the iceberg
This is just a taste of what you can do with Fabric.
How are you using it?
Deploying With Fabric
By Brandon Konkle
Deploying With Fabric
- 1,719