Getting started¶
Starting to provision servers with provy is extremely simple. We’ll provision a fairly scalable infrastructure (yet simple) for a Tornado website.
Tornado is facebook’s python web server. It features non-blocking I/O and is extremely fast.
The solution¶
Below you can see a diagram of our solution:
Our solution will feature a front-end server with one nginx instance doing the load-balancing among the tornado instances in our back-end server.
The back-end server will feature 4 tornado instances kept alive by supervisor (a process monitoring tool).
Create a file called website.py at some directory with this content:
#!/usr/bin/python
# -*- coding: utf-8 -*-
import sys
import tornado.ioloop
import tornado.web
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.write("Hello, world")
application = tornado.web.Application([
(r"/", MainHandler),
])
if __name__ == "__main__":
port = int(sys.argv[1])
application.listen(port, '0.0.0.0')
print ">> Website running at http://0.0.0.0:%d" % port
tornado.ioloop.IOLoop.instance().start()
Yes, it is not a very involved example, but Hello World suffices for our purposes. This python application takes a port as command-line argument and can be run with:
$ python website.py 8000
>> Website running at http://0.0.0.0:8000
The servers¶
Ok, now that we have a functioning application, let’s deploy it to production.
First, let’s create a local “production” environment using Vagrant. Using Vagrant is beyond the scope of this tutorial.
First make sure you have the base box installed. If you don’t, use:
$ vagrant box add base http://files.vagrantup.com/lucid32.box
In the same directory that we created the website.py file, type:
$ vagrant init
create Vagrantfile
This will create a file called VagrantFile. This is the file that configures our Vagrant instances. Open it up in your editor of choice and change it to read:
Vagrant::Config.run do |config|
config.vm.define :frontend do |inner_config|
inner_config.vm.box = "base"
inner_config.vm.forward_port(80, 8080)
inner_config.vm.network(:hostonly, "33.33.33.33")
end
config.vm.define :backend do |inner_config|
inner_config.vm.box = "base"
inner_config.vm.forward_port(80, 8081)
inner_config.vm.network(:hostonly, "33.33.33.34")
end
end
Ok, now when we run vagrant we’ll have two servers up: 33.33.33.33 and 33.33.33.34. The first one will be our front-end server and the latter our back-end server.
Provisioning file¶
It’s now time to start provisioning our servers. In the same directory that we created the website.py file, let’s create a file called provyfile.py.
The first thing we’ll do in this file is importing the provy classes we’ll use. We’ll also define FrontEnd and BackEnd roles and assign them to our two vagrant servers.
#!/usr/bin/python
# -*- coding: utf-8 -*-
from provy.core import Role
class FrontEnd(Role):
def provision(self):
pass
class BackEnd(Role):
def provision(self):
pass
servers = {
'test': {
'frontend': {
'address': '33.33.33.33',
'user': 'vagrant',
'roles': [
FrontEnd
]
},
'backend': {
'address': '33.33.33.34',
'user': 'vagrant',
'roles': [
BackEnd
]
}
}
}
Even though our script does not actually provision anything yet, let’s stop to see some interesting points of it.
You can see that our roles (FrontEnd and BackEnd) both inherit from provy.Role. This is needed so that these roles can inherit a lot of functionality needed for interacting with our servers.
Another thing to notice is the servers dictionary. This is where we tell provy how to connect to each server and what roles does it have.
We can run this script (even if it won’t do anything) with:
$ # will provision both servers
$ provy -s test
$ # will provision only the frontend server
$ provy -s test.frontend
$ # will provision only the backend server
$ provy -s test.backend
Provisioning the back-end server¶
Let’s start working in our back-end server, since our front-end server depends on it to run.
First we’ll make sure we are running our app under our own user and not root:
#!/usr/bin/python
# -*- coding: utf-8 -*-
from provy.core import Role
from provy.more.debian import UserRole
class FrontEnd(Role):
def provision(self):
pass
class BackEnd(Role):
def provision(self):
with self.using(UserRole) as role:
role.ensure_user('backend', identified_by='pass', is_admin=True)
servers = {
'test': {
'frontend': {
'address': '33.33.33.33',
'user': 'vagrant',
'roles': [
FrontEnd
]
},
'backend': {
'address': '33.33.33.34',
'user': 'vagrant',
'roles': [
BackEnd
]
}
}
}
Then we’ll need to copy the website.py file to the server. provy can easily copy files to the servers, as long as it can find them. Just move the website.py file to a directory named files in the same directory as provyfile.py.
Now we can easily copy it to the /home/frontend directory:
#!/usr/bin/python
# -*- coding: utf-8 -*-
from provy.core import Role
from provy.more.debian import UserRole
class FrontEnd(Role):
def provision(self):
pass
class BackEnd(Role):
def provision(self):
with self.using(UserRole) as role:
role.ensure_user('backend', identified_by='pass', is_admin=True)
self.update_file('website.py', '/home/backend/website.py', owner='backend', sudo=True)
servers = {
'test': {
'frontend': {
'address': '33.33.33.33',
'user': 'vagrant',
'roles': [
FrontEnd
]
},
'backend': {
'address': '33.33.33.34',
'user': 'vagrant',
'roles': [
BackEnd
]
}
}
}
The update_file method tells provy to compare the source and target files and if they are different update the target file. For more information check the documentation.
Next we must make sure Tornado is installed. provy already comes with a role that does that:
#!/usr/bin/python
# -*- coding: utf-8 -*-
from provy.core import Role
from provy.more.debian import UserRole, TornadoRole
class FrontEnd(Role):
def provision(self):
pass
class BackEnd(Role):
def provision(self):
with self.using(UserRole) as role:
role.ensure_user('backend', identified_by='pass', is_admin=True)
self.update_file('website.py', '/home/backend/website.py', owner='backend', sudo=True)
self.provision_role(TornadoRole)
servers = {
'test': {
'frontend': {
'address': '33.33.33.33',
'user': 'vagrant',
'roles': [
FrontEnd
]
},
'backend': {
'address': '33.33.33.34',
'user': 'vagrant',
'roles': [
BackEnd
]
}
}
}
Now all we have to do is instruct supervisor to run four instances of our app:
#!/usr/bin/python
# -*- coding: utf-8 -*-
from provy.core import Role
from provy.more.debian import UserRole, TornadoRole, SupervisorRole
class FrontEnd(Role):
def provision(self):
pass
class BackEnd(Role):
def provision(self):
with self.using(UserRole) as role:
role.ensure_user('backend', identified_by='pass', is_admin=True)
self.update_file('website.py', '/home/backend/website.py', owner='backend', sudo=True)
self.provision_role(TornadoRole)
# make sure we have a folder to store our logs
self.ensure_dir('/home/backend/logs', owner='backend')
with self.using(SupervisorRole) as role:
role.config(
config_file_directory='/home/backend',
log_folder='/home/backend/logs/',
user='backend'
)
with role.with_program('website') as program:
program.directory = '/home/backend'
program.command = 'python website.py 800%(process_num)s'
program.number_of_processes = 4
program.log_folder = '/home/backend/logs'
servers = {
'test': {
'frontend': {
'address': '33.33.33.33',
'user': 'vagrant',
'roles': [
FrontEnd
]
},
'backend': {
'address': '33.33.33.34',
'user': 'vagrant',
'roles': [
BackEnd
]
}
}
}
Provisioning the front-end server¶
Ok, now let’s get our front-end up and running. provy comes with an nginx module, so it is pretty easy configuring it.
We have to provide template files for both nginx.conf and our website’s site. Following what Tornado‘s documentation instructs, these are good templates:
user {{ user }};
worker_processes 1;
error_log /home/frontend/error.log;
pid /home/frontend/nginx.pid;
events {
worker_connections 1024;
use epoll;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
access_log /home/frontend/nginx.access.log;
keepalive_timeout 65;
proxy_read_timeout 200;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
gzip on;
gzip_min_length 1000;
gzip_proxied any;
gzip_types text/plain text/css text/xml
application/x-javascript application/xml
application/atom+xml text/javascript;
proxy_next_upstream error;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
upstream frontends {
server 33.33.33.34:8000;
server 33.33.33.34:8001;
server 33.33.33.34:8002;
server 33.33.33.34:8003;
}
server {
listen 8888;
server_name localhost 33.33.33.33;
access_log /home/frontend/website.access.log;
location / {
proxy_pass_header Server;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Scheme $scheme;
proxy_pass http://frontends;
}
}
Save them as files/nginx.conf and files/website, respectively.
Now all that’s left is making sure that provy configures our front-end server:
#!/usr/bin/python
# -*- coding: utf-8 -*-
from provy.core import Role
from provy.more.debian import UserRole, TornadoRole, SupervisorRole, NginxRole
class FrontEnd(Role):
def provision(self):
with self.using(UserRole) as role:
role.ensure_user('frontend', identified_by='pass', is_admin=True)
with self.using(NginxRole) as role:
role.ensure_conf(conf_template='nginx.conf', options={'user': 'frontend'})
role.ensure_site_disabled('default')
role.create_site(site='website', template='website')
role.ensure_site_enabled('website')
class BackEnd(Role):
def provision(self):
with self.using(UserRole) as role:
role.ensure_user('backend', identified_by='pass', is_admin=True)
self.update_file('website.py', '/home/backend/website.py', owner='backend', sudo=True)
self.provision_role(TornadoRole)
# make sure we have a folder to store our logs
self.ensure_dir('/home/backend/logs', owner='backend')
with self.using(SupervisorRole) as role:
role.config(
config_file_directory='/home/backend',
log_folder='/home/backend/logs/',
user='backend'
)
with role.with_program('website') as program:
program.directory = '/home/backend'
program.command = 'python website.py 800%(process_num)s'
program.number_of_processes = 4
program.log_folder = '/home/backend/logs'
servers = {
'test': {
'frontend': {
'address': '33.33.33.33',
'user': 'vagrant',
'roles': [
FrontEnd
]
},
'backend': {
'address': '33.33.33.34',
'user': 'vagrant',
'roles': [
BackEnd
]
}
}
}
See how we passed the user name as an option to the nginx.conf template? provy allows this kind of template interaction in many places. For more information, check the documentation.
Running and verifying it works¶
We can now fire our brand new infrastructure and check that the website is working:
$ vagrant up
$ provy -s test
$ curl http://33.33.33.33
After these 3 commands finished running (it might take a long time depending on your connection speed), you should see Hello World as the result of the curl command.