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: .. image:: images/provy_sample.png 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: .. code-block:: bash $ 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: .. code-block:: bash $ vagrant box add base http://files.vagrantup.com/lucid32.box In the same directory that we created the website.py file, type: .. code-block:: bash $ 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: .. code-block:: ruby 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: .. code-block:: bash $ # 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: .. code-block:: nginx 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/*; } .. code-block:: nginx 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: .. code-block:: bash $ 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.